Merge branch 'react-rewrite' 🎉

This commit is contained in:
Roman Hotsiy 2018-03-21 17:32:28 +02:00
commit 0382034ec2
No known key found for this signature in database
GPG Key ID: 5CB7B3ACABA57CB0
346 changed files with 48091 additions and 16200 deletions

View File

@ -1,30 +1,10 @@
root = true
[*]
charset=utf-8
end_of_line=lf
insert_final_newline=false
indent_style=space
indent_size=2
[{.eslintrc,.babelrc,.stylelintrc,jest.config,*.json,*.jsb3,*.jsb2,*.bowerrc}]
indent_style=space
indent_size=2
[*.scss]
indent_style=space
indent_size=2
[*.styl]
indent_style=space
indent_size=2
[*.coffee]
indent_style=space
indent_size=2
[{.analysis_options,*.yml,*.yaml}]
indent_style=space
indent_size=2
[tslint.json]
indent_style=space
indent_size=2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
indent_style = space
indent_size = 2
trim_trailing_whitespace = true

94
.github/CONTRIBUTING.md vendored Normal file
View File

@ -0,0 +1,94 @@
# ReDoc Contributing Guide
Hi! We're really excited that you are interested in contributing to ReDoc. Before submitting your contribution though, please make sure to take a moment and read through the following guidelines.
- [Issue Reporting Guidelines](#issue-reporting-guidelines)
- [Pull Request Guidelines](#pull-request-guidelines)
- [Development Setup](#development-setup)
- [Project Structure](#project-structure)
## Issue Reporting Guidelines
- Before filing a new issue, try to make sure your problem doesnt already exist.
- The best way to get your bug fixed is to provide a reduced test case.
## Pull Request Guidelines
Before submitting a pull request, please make sure the following is done:
1. Fork the repository and create your branch from master.
2. Run `yarn` in the repository root.
3. If youve fixed a bug or added code that should be tested, add tests!
4. Ensure the test suite passes (`yarn test`). Tip: `yarn test --watch TestName` is helpful in development.
5. Format your code with prettier (`yarn prettier`).
## Development Setup
You will need [Node.js](http://nodejs.org) at `v8.0.0+` and [Yarn](https://yarnpkg.com/en/) at `v1.2.0+`
After cloning the repo, run:
```bash
$ yarn install # or yarn
```
### Commonly used NPM scripts
``` bash
# dev-server, watch and auto reload playground
$ yarn start
# start playground app in production environement
$ yarn start:prod
# runt tslint
$ yarn lint
# try autofix tslint issues
$ yarn lint --fix
# run unit tests
$ yarn unit
# run e2e tests
$ yarn e2e
# open cypress UI to debug e2e test
$ yarn cy:open
# run the full test suite, include linting / unit / e2e
$ yarn test
# prepare bundles
$ yarn bundle
# format the code using prettier
$ yarn prettier
# auto-generate changelog
$ yarn changelog
```
There are some other scripts available in the `scripts` section of the `package.json` file.
## Project Structure
- **`benchmark`**: contains basic perf benchmark. Not fully ready yet
- **`demo`**: contains project demo with demo specs and HMR playground used in development
- `demo/playground`: HMR Playground used in development
- **`docs`**: contains extra docs (linked from README.md)
- **`e2e`**: contains e2e tests. The e2e tests are written and run with [Cypress](https://www.cypress.io/).
- **`src`**: contains the source code. The codebase is written in Typescript. CSS styles are managed with [Styled components](https://www.styled-components.com/). State is managed by [MobX](https://github.com/mobxjs/mobx)
- **`src/common-elements`**: containts common Styled elements or components used in multiple places
- **`src/components`**: contains main visual components
- **`src/services`**: contains different services used by ReDoc including MobX stores
- **`src/services/models`**: contains classes for OpenAPI entities (e.g. Response, Operations, etc)
- **`src/types`**: contains extra typescript typings including OpenAPI doc typings
- **`src/utils`**: utility functions
- **`src/styled-components.ts`**: - reexprots styled-components with proper typescript annotations using theme
- **`src/theme.ts`**: - default theme (colors, fonts, etc) used by all the components

19
.gitignore vendored
View File

@ -20,20 +20,15 @@ npm-debug.log*
# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git
node_modules
# compiled css
lib/**/*.css
lib/
stats.json
e2e/.build/
cypress/
bundles
cli/index.js
# files produced by ngc
lib/**/*.ngfactory.ts
lib/**/*.css.shim.ts
**/*.ngsummary.json
lib/**/*.shim.ngstyle.ts
/benchmark/revisions
# other
/dist
/demo/build
.tmp
compiled
/coverage
.ghpages-tmp
stats.json

View File

@ -1,12 +1,3 @@
.DS_Store
**/.*
compiled
node_modules
jspm_packages
tests
lib
demo
build
coverage
!bundles/
!package.json
!README.md

View File

@ -1,41 +1,34 @@
language: node_js
node_js:
- '6'
branches:
except:
- releases
matrix:
include:
- env: JOB=unit
- env: JOB=e2e-guru
fast_finish: true
allow_failures:
- env: JOB=e2e-guru
- '8'
cache: yarn
env:
global:
- SAUCE_USERNAME: redoc
- secure: OgYIXlyF3tvHVrMjqIMR7UPhNvMzNZRkKE++LsQPUT4aoUmHoeukjgE9tKCZac3Z+PcAlgHQ6dPUqjqij2d/emYBIrCx4ABvTOnxQYHbk+8GriwPvQjV33MCfKvRQAf3XPNcga02aOO/EEGejrtY/i5sz86XPZFXap4scxbvnF0rzOwGU9/A40JLsvKjtfdRxJ3aC2QBnUHP9JkfBOzbs3jjwEWYN7HdUBqtYU+wiUwvHhpEPQ8BVNF5ETp5OxxkTdqJlMRf3azC4U/Rl8dhUXZP2l1ZBdpgahDvSkx1zXPwKRg1jos88jahqH/v8DLHtXNuYQy4S48nKWZfaRtQegG/XsYAftYPtYage7L9D0nFQW9YZI0vwUBEzh2YTf+QNpVwUSyZhVuS1oS+scTb8RU8BVVZd6hRpJiaI2YiPM6ZFN7a69deV0cASov3yI7GSfeq4MmB4hu72Le+GhemQMLRxNgtr32c18XW1XoHEnpV7s0eKXnZrevnM06tW/nX9TQXGloaMUeQzoZY0AuF00zmrHDaNgwT63ULjY0W591Ey1Ztfq4ixdCoi6IPAtl49vTnNMMMqatI7Si/haI6hGyTy+H0xK1GlUpBCe5CGusXDSKUqrlKS5Izreepfj1G+12391ixiapkwV3SbYdQEhcDt7HWupyI4Zfkvg8EwwE=
- GH_REF: github.com/Rebilly/ReDoc.git
- GIT_AUTHOR_EMAIL: redoc-bot@users.noreply.github.com
- GIT_AUTHOR_NAME: RedocBot
- secure: H2GClDJ7TEQaWgnk8d2fIVDpLwG3rTmN8TalUzrCqXGoG6ylCVmlwzKLgfPPWrVgSA7QTdfNV0ab7c2KyPoZBinHmeGMSuKNLVbhOXRc2VFxTBntBTuyJhfCgQEpUKvJesJUuv5RuBn//wC7VZcVVNc06TZDEe8+aDVYQuCRMXZJ4X3e6nSZ64QX2veyVcr+TjnRsZPkeBVcK9hngvuaxLb/hbJ85CvjiseZRt47PGIcrEpMn9n2GMw1m0fNnPoN+MBTSCnIklTmdjPG7t4GUSLmD6H0lNLdXuehYqmQAHgYrLec1aiFlV57QaDoDZrq2hSf4vDmCB/FVydGhD5JunI67pujoV2OnD1V80eUZhYNWOYsJ2Nfp4NxgXsPUcE6zWLYsLfktMPZADhOXInQRACt1cnx8zMYKLnch1RY/ZqjSg0nPtRjLzQ0lNsw5leixvBdBnMjxYHVyAWVwg8WiJMaLO9vog2Qnxg1NTacHO2CsOmm2rw6stpg7ndp/+nOleRlfUKggjt0Tn3FjwCIXeGup2P2EBa+WW2YMAaoMFofYviR5vRlKBgdKo9fsAruaO1r6nm2EdAjOlniyw92bEfU/qOey1nVp/oK2S82uT5In8KB7vl6rF3ak7WAsT9Q5vZUhsrG+eE4PVyIyWNBhs4A7pSwZGHDR/MYtp0E2ug=
- secure: apiavCfCQngL9Een1m7MIXMf3bqO3rY4YY59TMBl/yFKi80CEsHPHhgVUkl6hC+aM5PeBt/vgjh37rHMX31j/pcSZ4Z8SO/4Bwr36iHfhSxSEuAQog8P07qWqH7wYYWGIVmF682stgl0fYF+GN92sx/6edFVzsWVECf2G7imtICKSTbhKGm3Dhn2JwGnhD7eyfgZ33omgiaswumdu0xABoXDfqSZR+16fC4Ap5rhv3fXO9ndvRNy1STn376nT+my6e86UrQL4aS/S+HNHgIe1BUs+5cOp6Jgw6t0ie7phY0EAiECsRxy9K4e3Dctv9m6+Wma4+vy65MS0zGyrqey6oyV4l827sCOjrD1qcqc9bX6FlMSouVoNfE4ZjINNAbgigTaiLSoDSPcf5I5smkkM2ezzFOMSZwZxNdaNL2LKb97vc8m/ZUkv0sKZyT7oqVL7aJweEivsSHj5l2KR8Z7XrVB1y2eI6GvyTSa/d+CL4dSRzjh8+IRN047YBrdTKD5IkdT0upfoBu14WPUfFmLKxX+iMCslXRWb6kwojhrWNYmZvL65KRAzJ6+eIPDG/W5QUOpYyYT77bLlBQjVo6NmVvl9v3HMECq9CHH0ivKFBGPiKMOx7cJkTax3FuyznOW2WCXB9kTb5Zk9toaiNlSp9L6ll/h2Eyxa6n6sWUgmmM=
- secure: vVRg9BKGBwF2MbXQnEccFL+XW0/7RaBmge9k7jbGYScBwkP3XjnQ/Xaj0cvTz2CM2EqXsbpwfvr4Jo+enW/E3MGy5RiEzv5hUe/jIFRR0gfAFbZxSTvg5xiFhTDffqQk0fncO4jXu+wPO5lZ2CMRWzyXz3i1MZhjMcAgoDr1+TRss/EGXLNHxr2RM88tpUW0fV2prIRoyGqhCgnYZtrm7hmr41Ej+itg1MqZLml/Rjkt3KsNgI+z0O5Qn3QSAO8GtPZqeftQxAjevOmxZGcssxY8EJvqbjAujr4y51WncXpEmCRPSY2J9R5+fkgZurqwnJapbQpjwKYemok3ps7EHg2gWkAlmPdQO4LKpbffGkM/o5b+8+HdIuQZugsSWQD9hUSftTAFLcfA1isi7V2lHE1m8bX/vk9zIyDdcPSwIaFe9y+w3PexwFmTjPLq+nia/UY2kARFZMEIFAJby6gkA70DcAJ50QOM86InJu5DSzGbIssgTGAXCn0TPPyGveaurVLw8x61j3yh8LDF46gUHey3rqv6WjpCM9h/vg7X/gq5ve/5Q2KHscUKfs/sA53Mt7qPeqRZY1QCaaRjzqJO/ZraHqWWeKmPKaWhPGR0kYEnkvB+K9GZ+HNSWCltjCO4SJ1xeEl7CRqQxAwdiMATF5SKqyiC+bn5oc35mFgbRF8=
- secure: ela1tn4wkJQZ8O4iv+4pIZi5cebxeCStVF1tEUe6qa6WWgJYVXmS2tEv3QQ36NUBFrP58Y6yl10XguPnvj/2BCqcZI4FUBHh3BfiBoUtXxDCVKI5LtlniNiOFGUwfzEeYka8T51zFlcUXSCCaxHkRZbmBsIzeJ39UwTi5fy0qwLv9GgL0czhwm8I8sZ8gyWdGmqpXNFEsb9JP4ZA3mw2qpWkGpGAqQPD9XSCkU3LmX1/ltwsBMAgGYKLLo7vU8d5KV2c8L1Gnxfl6BvfmqUD/dsas/1rnk08rU2nez5ekuQa2tJRkDLOv8bqvrGRLjHSUa3yPuisC6SsDGSU7/3DcozZyYsz7WQ6WI8tYabyjqqeJTF1N8a5T3IbZaZNV1J4JHOO9Cb/y7gIg4edANg6tbe7MzZpdEPRBnw6OkdTdirpNsWQ/jnfpY1hn6mraQZz/q8yaz3W21NjbBJhVnvfh5gWLKQ3YAAziCBhmmrThFhUu0czz+G920MuFo477TBcxvlrE7CaNJ0Q6yYkDehEPOv3jvEs1QVHPwuRrlaLTbBhrlTICKZ58gdX30O8N4i0Xgp/v6qrC03bplnMQc8E/uC61wcVLJixnlZVp8FODpUvPjsxVFkpuNSOIAaiqcERmoiPXx05Epzmr78hjU5rYCx/1MmVoeB4gs9YO+4guD4=
- secure: YWBKHe3EdY3DcASsMaW3/scxba11eMRjJ8HebyKyampU9JTkyj3rFY9sfYQshusYCVa3/pt2LyYITNLDI2vYGGFYVIu9ia9kMBjt4kZ99QQxvXTftmBruF2Gcw6aqja9e5SnQrDUY+Wnc3k33SqQhhNGiyF6M1HpV0S8JJZNc3oQjZHKkZ93IwA1mU/UWL/K3wE/y5xiKz8d+DjsLrhU94G6NE2mhs0eaJW/fMLLYxn+UfDuidGaRQ4v4a2aRPecalzyLqdRFgFH2YJpBPFF3XEC23J/hm1QChI75iINChWbTrNTJqWYKKljjB7WLNtqVrm3S5qpZwl322nwBhwhPVtT4PqattMribCA8XMSL9Lk/g1hRaxH7gXgGHAfpPaMhyPcKBzQ5HJxYI+UXoKPZqfyxkZbnVY/3s7P0pZ8Nql+UouvFURZkbWK3WLWCISCVVVdyhnB3wn+jKLWdsYCtu9ibvYDDCDT64T08b4Gf30j3BfaAlgO4uU30Bg/IulrFnOC99qhslQRODxY6HGizr6ggtS1C8TFZ2/+3CXuHZJ3LDdj9WoHyBgYtMtnQf+qBVy6z6nvyx/QsdnQ/8A1twObuZoFCLPpOPoKmAbjApC8XS0TgIyqf5KGcq33Jxg+bIakNAGJW33n4jtZW7+Z+p8SVGLQmRC/558Tn402a58=
addons:
sauce_connect: true
chrome: stable
cache: yarn
before_install: if [[ `npm -v` != 3* ]]; then npm i -g npm@3; fi
before_deploy:
- if [[ ! -z "$TRAVIS_TAG" ]]; then npm run build:all; fi
before_script: npm run bundle && npm run compile:cli
script: npm test && npm run e2e-ci
after_script: cat ./coverage/lcov.info | coveralls
before_deploy: npm run compile:cli
deploy:
- provider: npm
skip_cleanup: true
email: gotsijroman@gmail.com
api_key:
secure: PuhWLERrCEFmXmdFpw2OVFlqpOIVDmgwk5JUJOYaFdVCh/smp0+jZCQ4vrdFpuG96rnDVirD+A8xvW6NgsNNaRthLgOB/LRdFN69rU6Gvn3At6wlnC55t5dlhxPvCfnzJcHVBLXX4EmMkjnZqDg2uczXTzPodr3FnQJNuXmP8B33fzDVLyHccvXZ90abwXWVrgRIXPU28niqCR8DOC2OTzs7wqz+BLNkYDRRbyYXsg62HWuD33x5iof5IqBmhzBt3usCGmF3QGcgHrXHdZw3sZnit8+Bua++3KrXR0x6HGXXN1AoXVmCAkCa5OTQ5R3tCRxiJN3P2KLnvWeZR74sTFkovJB/6pGCvbJ/c7Wnuw6sD7SgOUBD359ULB6lAf5OnxBLoNebX4JxxVXF+zA4E3Bl44VxkzDpPWc15xqBPMB5vBREzMVmJ5mExn2s5cmLQjADbl9h0y6gZnhnNJ+iTmqtrVyM0ZkF2rPrzrTdGD+ULmRIlTMkdD1bh+/TJ3RdXT3P4/zNUJmiNnvgnnJVYYvsGaXWF+7uCVHT/8k2RsoSHqgkqh0gkDqGSwVix55y5mC7T2Vk9lMBhm6MvFJXaonOX0kxJS4EDQ3plPd6/ybG+TLhwggYnQ8o9msU5Nt6FpUShKiezjKurIhbQZdwlVivX3tahjW2QjNDO58xGgY=
on:
tags: true
condition: $JOB != e2e-guru
- skip_cleanup: true
provider: script
script: npm run deploy
on:
tags: true
condition: $JOB != e2e-guru
- provider: npm
skip_cleanup: true
email: gotsijroman@gmail.com
tag: next
api_key: $NPM_TOKEN
on:
tags: true
- provider: script
skip_cleanup: true
script: cd cli && yarn install && npm run ci-publish || true
on:
tags: true
# TODO demo deployment to GH pages

4
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,4 @@
{
"editor.formatOnSave": true,
"typescript.tsdk": "node_modules/typescript/lib"
}

View File

@ -1,3 +1,228 @@
<a name="2.0.0-alpha.15"></a>
# [2.0.0-alpha.15](https://github.com/Rebilly/Redoc/compare/v2.0.0-alpha.14...v2.0.0-alpha.15) (2018-03-16)
### Bug Fixes
* fix broken css after installing polished ([6018042](https://github.com/Rebilly/Redoc/commit/6018042))
### Features
* more advanced theme engine ([1df690a](https://github.com/Rebilly/Redoc/commit/1df690a))
<a name="2.0.0-alpha.14"></a>
# [2.0.0-alpha.14](https://github.com/Rebilly/Redoc/compare/v2.0.0-alpha.13...v2.0.0-alpha.14) (2018-03-15)
### Bug Fixes
* fix CLI crash + build it on travis ([7769ba8](https://github.com/Rebilly/Redoc/commit/7769ba8))
<a name="2.0.0-alpha.13"></a>
# [2.0.0-alpha.13](https://github.com/Rebilly/Redoc/compare/v2.0.0-alpha.12...v2.0.0-alpha.13) (2018-03-15)
### Bug Fixes
* A couple minor bug fixes ([#436](https://github.com/Rebilly/Redoc/issues/436)) ([5dc21af](https://github.com/Rebilly/Redoc/commit/5dc21af))
* add extra null-check + warning ([8757fa5](https://github.com/Rebilly/Redoc/commit/8757fa5))
* add logo width to the theme ([28f2391](https://github.com/Rebilly/Redoc/commit/28f2391))
* align logo by center ([18ec3ac](https://github.com/Rebilly/Redoc/commit/18ec3ac))
* discriminator dropdown showing incorrect field if sorted ([bcf39dc](https://github.com/Rebilly/Redoc/commit/bcf39dc))
* fix crash when referencing non-existing security scheme ([1f7fc44](https://github.com/Rebilly/Redoc/commit/1f7fc44))
* fix overflowing content in JSON samples ([02c2413](https://github.com/Rebilly/Redoc/commit/02c2413))
* fix right-panel blinking when scrolling + css improvements ([a78f9ab](https://github.com/Rebilly/Redoc/commit/a78f9ab))
* fix search-indexing for SSR ([1428fb5](https://github.com/Rebilly/Redoc/commit/1428fb5))
* fix the media queries utils so it gets the values from the current theme ([#420](https://github.com/Rebilly/Redoc/issues/420)) ([3924d3c](https://github.com/Rebilly/Redoc/commit/3924d3c))
* fix worker import ([4896346](https://github.com/Rebilly/Redoc/commit/4896346))
* make ReactStandalone react on props changes ([0cb0af2](https://github.com/Rebilly/Redoc/commit/0cb0af2))
* merge inner properties of allOf ([8926dd4](https://github.com/Rebilly/Redoc/commit/8926dd4))
* one-of dropdown not switching ([0f1b6a6](https://github.com/Rebilly/Redoc/commit/0f1b6a6))
* referenced header name is empty ([13165fb](https://github.com/Rebilly/Redoc/commit/13165fb))
* skipReadOnly/skipWritOnly not passing down to nested array ([6df8127](https://github.com/Rebilly/Redoc/commit/6df8127))
* skipReadOnly/skipWritOnly not passing down to nested OneOf ([2462639](https://github.com/Rebilly/Redoc/commit/2462639))
* various search fixes ([b797c96](https://github.com/Rebilly/Redoc/commit/b797c96))
* writeOnly not respected in response samples ([87abdf7](https://github.com/Rebilly/Redoc/commit/87abdf7))
### Features
* add clear icon to searchbox ([825162e](https://github.com/Rebilly/Redoc/commit/825162e))
* add hideDownloadButton option ([8dbe938](https://github.com/Rebilly/Redoc/commit/8dbe938))
* add marker ([1ff2bd8](https://github.com/Rebilly/Redoc/commit/1ff2bd8))
* arrow navigation in search results ([fe3245a](https://github.com/Rebilly/Redoc/commit/fe3245a))
* basis search ([6990cd2](https://github.com/Rebilly/Redoc/commit/6990cd2))
* ReDoc CLI ✨ ([390f6c1](https://github.com/Rebilly/Redoc/commit/390f6c1))
* reqired-first sort order for params ([ecf33d2](https://github.com/Rebilly/Redoc/commit/ecf33d2))
* serialize search-index ([e94f842](https://github.com/Rebilly/Redoc/commit/e94f842))
<a name="2.0.0-alpha.12"></a>
# [2.0.0-alpha.12](https://github.com/Rebilly/Redoc/compare/v2.0.0-alpha.11...v2.0.0-alpha.12) (2018-02-07)
### Bug Fixes
* basic responsiveness ([a29c3cc](https://github.com/Rebilly/Redoc/commit/a29c3cc))
* crash in MarkdownRenderer on non-string ([dead161](https://github.com/Rebilly/Redoc/commit/dead161))
* discriminator fix ([ff3bb24](https://github.com/Rebilly/Redoc/commit/ff3bb24))
* filter out non-existing security schemas + warn ([ee822f6](https://github.com/Rebilly/Redoc/commit/ee822f6))
* fix oneOf title for array ([1f3701d](https://github.com/Rebilly/Redoc/commit/1f3701d))
* fix tbody > tr nesting warning ([a3cbb14](https://github.com/Rebilly/Redoc/commit/a3cbb14))
* improve copy tooltip perf ([29207cf](https://github.com/Rebilly/Redoc/commit/29207cf))
* resolve menu synchronization issue (use proper throttle) ([84d1c7b](https://github.com/Rebilly/Redoc/commit/84d1c7b))
### Features
* responsive side menu ([3aab2d9](https://github.com/Rebilly/Redoc/commit/3aab2d9))
<a name="2.0.0-alpha.11"></a>
# [2.0.0-alpha.11](https://github.com/Rebilly/Redoc/compare/v2.0.0-alpha.10...v2.0.0-alpha.11) (2018-01-29)
### Bug Fixes
* courier misspelling ([#409](https://github.com/Rebilly/Redoc/issues/409)) ([96fb7ce](https://github.com/Rebilly/Redoc/commit/96fb7ce))
* crash on 2-level md heading at the beginning ([e9f23f7](https://github.com/Rebilly/Redoc/commit/e9f23f7))
* make active tab more clear ([4b5df22](https://github.com/Rebilly/Redoc/commit/4b5df22))
* perfect scroll not working ([199f240](https://github.com/Rebilly/Redoc/commit/199f240))
* use array items example ([12f79f0](https://github.com/Rebilly/Redoc/commit/12f79f0)), closes [#408](https://github.com/Rebilly/Redoc/issues/408)
* wrap text in code samples ([6c71a66](https://github.com/Rebilly/Redoc/commit/6c71a66))
### Features
* port "copy to clipboard" / "expand/collapse all" functionality ([5bb0bdf](https://github.com/Rebilly/Redoc/commit/5bb0bdf)), closes [#410](https://github.com/Rebilly/Redoc/issues/410)
<a name="2.0.0-alpha.9"></a>
# [2.0.0-alpha.9](https://github.com/Rebilly/Redoc/compare/v2.0.0-alpha.8...v2.0.0-alpha.9) (2018-01-11)
### Bug Fixes
* handle scrollYOffset in ScrollService ([dcab770](https://github.com/Rebilly/Redoc/commit/dcab770))
<a name="2.0.0-alpha.8"></a>
# [2.0.0-alpha.8](https://github.com/Rebilly/Redoc/compare/v2.0.0-alpha.7...v2.0.0-alpha.8) (2018-01-10)
### Bug Fixes
* undo section id + some minor fixes ([0253c5d](https://github.com/Rebilly/Redoc/commit/0253c5d))
<a name="2.0.0-alpha.7"></a>
# [2.0.0-alpha.7](https://github.com/Rebilly/Redoc/compare/v2.0.0-alpha.6...v2.0.0-alpha.7) (2018-01-10)
### Bug Fixes
* add id attr to headers to work before react is loaded if ssr ([1743453](https://github.com/Rebilly/Redoc/commit/1743453))
* crate spec as data/base64 link when ssr ([33678e6](https://github.com/Rebilly/Redoc/commit/33678e6))
* example value is not showed if it is false ([9756364](https://github.com/Rebilly/Redoc/commit/9756364))
<a name="2.0.0-alpha.6"></a>
# [2.0.0-alpha.6](https://github.com/Rebilly/Redoc/compare/v2.0.0-alpha.5...v2.0.0-alpha.6) (2018-01-10)
### Bug Fixes
* allOf and deref exit not only named refs ([435cccd](https://github.com/Rebilly/Redoc/commit/435cccd))
* do not ignore path level parameters ([14f8408](https://github.com/Rebilly/Redoc/commit/14f8408))
* improve rendering of types ([17da7b7](https://github.com/Rebilly/Redoc/commit/17da7b7))
* move title propagation to the correct place ([0b0bc99](https://github.com/Rebilly/Redoc/commit/0b0bc99))
* owerwrite text-align to left ([bfee3ed](https://github.com/Rebilly/Redoc/commit/bfee3ed))
### Features
* initial display security requirements ([50e2a58](https://github.com/Rebilly/Redoc/commit/50e2a58))
<a name="2.0.0-alpha.5"></a>
# [2.0.0-alpha.5](https://github.com/Rebilly/Redoc/compare/v2.0.0-alpha.4...v2.0.0-alpha.5) (2017-12-07)
### Bug Fixes
* correct pointer for the schema ([4ae1574](https://github.com/Rebilly/Redoc/commit/4ae1574))
* bundle in reftools in lib build (do not crash on prod builds in create-react-app) ([57129d3](https://github.com/Rebilly/Redoc/commit/57129d3))
<a name="2.0.0-alpha.4"></a>
# [2.0.0-alpha.4](https://github.com/Rebilly/Redoc/compare/v2.0.0-alpha.3...v2.0.0-alpha.4) (2017-11-24)
### Bug Fixes
* add ellipsis for menu items with long words ([3421be2](https://github.com/Rebilly/Redoc/commit/3421be2))
* crashes on some dereferencing/allOf merging cases ([335deb9](https://github.com/Rebilly/Redoc/commit/335deb9))
* do not auto-append security-definitions if they are not in the spec ([426e5b6](https://github.com/Rebilly/Redoc/commit/426e5b6))
* don't display operations without tags as tag items in menu ([ca81b6d](https://github.com/Rebilly/Redoc/commit/ca81b6d))
<a name="2.0.0-alpha.3"></a>
# [2.0.0-alpha.3](https://github.com/Rebilly/Redoc/compare/v2.0.0-alpha.2...v2.0.0-alpha.3) (2017-11-23)
### Bug Fixes
* crash when $ref is url encoded ([bdf6079](https://github.com/Rebilly/Redoc/commit/bdf6079))
* make oneOf not skip fields defined alongside ([8680775](https://github.com/Rebilly/Redoc/commit/8680775))
<a name="2.0.0-alpha.2"></a>
# 2.0.0-alpha.2 (2017-11-23)
### Bug Fixes
* Fix crash when using type `file` in OpenAPI 2.0 in some places
<a name="2.0.0-alpha.1"></a>
# 2.0.0-alpha.1 (2017-11-23)
Complete rewrite of ReDoc using React so here only major changes are listed.
Complete rewrite also means that this rewrite may introduce issues, but they should be resolved before `2.0.0`.
### Features
- Basic Support for OpenAPI 3
- Usage as a React component
### Deprecations
- Fonts are not loaded by ReDoc so you should load them. Default fonts can be loaded as bellow:
```html
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
```
- no more bower releases
- no more GitHub pages-based CDN. Use [unpkg.com](https://unpkg.com/) to access ReDoc releases
### Known Regression (will be resolved before leaving alpha stage)
- `lazyLoading` option not implemented yet
- Copying to clipboard of samples not implemented yet
- Search not implemented yet
<a name="1.21.2"></a>
## [1.21.2](https://github.com/Rebilly/ReDoc/compare/v1.21.1...v1.21.2) (2018-02-26)
@ -51,6 +276,7 @@
### Deprecations
* Dropped bower support. No more dist files on the `releases` branch.
<a name="1.19.3"></a>
## [1.19.3](https://github.com/Rebilly/ReDoc/compare/v1.19.2...v1.19.3) (2017-11-16)
@ -71,11 +297,19 @@
<a name="1.19.1"></a>
# [1.19.1](https://github.com/Rebilly/ReDoc/compare/v1.19.0...v1.19.1) (2017-10-02)
<a name="2.0.0-alpha.16"></a>
# [2.0.0-alpha.16](https://github.com/Rebilly/Redoc/compare/v2.0.0-alpha.15...v2.0.0-alpha.16) (2018-03-18)
### Bug Fixes
* snapshot crashing on `constructor` prop ([04e8606](https://github.com/Rebilly/ReDoc/commit/04e8606)), closes [#341](https://github.com/Rebilly/ReDoc/issues/341)
* move cli to a separate npm package ([95c7585](https://github.com/Rebilly/Redoc/commit/95c7585))
* prefer `.extend` over `styled()` to make styles more predictable ([ed20ac1](https://github.com/Rebilly/Redoc/commit/ed20ac1))
### Features
* use new Context API for options ([e022349](https://github.com/Rebilly/Redoc/commit/e022349))
<a name="1.19.0"></a>

143
README.md
View File

@ -1,21 +1,21 @@
<p align="center">
<img alt="ReDoc logo" src="/docs/images/redoc-logo.png" width="300px" />
</p>
<p align="center">
<b>OpenAPI/Swagger-generated API Reference Documentation</b>
</p>
<div align="center">
<img alt="ReDoc logo" src="https://raw.githubusercontent.com/Rebilly/ReDoc/master/docs/images/redoc-logo.png" width="400px" />
[![Build Status](https://travis-ci.org/Rebilly/ReDoc.svg?branch=master)](https://travis-ci.org/Rebilly/ReDoc) [![Coverage Status](https://coveralls.io/repos/Rebilly/ReDoc/badge.svg?branch=master&service=github)](https://coveralls.io/github/Rebilly/ReDoc?branch=master) [![Tested on APIs.guru](http://api.apis.guru/badges/tested_on.svg)](https://APIs.guru) [![dependencies Status](https://david-dm.org/Rebilly/ReDoc/status.svg)](https://david-dm.org/Rebilly/ReDoc) [![devDependencies Status](https://david-dm.org/Rebilly/ReDoc/dev-status.svg)](https://david-dm.org/Rebilly/ReDoc#info=devDependencies) [![Stories in Ready](https://badge.waffle.io/Rebilly/ReDoc.png?label=ready&title=Ready)](https://waffle.io/Rebilly/ReDoc)
**OpenAPI/Swagger-generated API Reference Documentation**
[![Average time to resolve an issue](http://isitmaintained.com/badge/resolution/Rebilly/redoc.svg)](http://isitmaintained.com/project/Rebilly/redoc "Average time to resolve an issue") [![Percentage of issues still open](http://isitmaintained.com/badge/open/REBILLY/REDOC.svg)](http://isitmaintained.com/project/REBILLY/REDOC "Percentage of issues still open")
[![Build Status](https://travis-ci.org/Rebilly/ReDoc.svg?branch=master)](https://travis-ci.org/Rebilly/ReDoc) [![Coverage Status](https://coveralls.io/repos/Rebilly/ReDoc/badge.svg?branch=master&service=github)](https://coveralls.io/github/Rebilly/ReDoc?branch=master) [![dependencies Status](https://david-dm.org/Rebilly/ReDoc/status.svg)](https://david-dm.org/Rebilly/ReDoc) [![devDependencies Status](https://david-dm.org/Rebilly/ReDoc/dev-status.svg)](https://david-dm.org/Rebilly/ReDoc#info=devDependencies) [![npm](http://img.shields.io/npm/v/redoc.svg)](https://www.npmjs.com/package/redoc) [![License](https://img.shields.io/npm/l/redoc.svg)](https://github.com/Rebilly/ReDoc/blob/master/LICENSE)
[![npm](http://img.shields.io/npm/v/redoc.svg)](https://www.npmjs.com/package/redoc) [![License](https://img.shields.io/npm/l/redoc.svg)](https://github.com/Rebilly/ReDoc/blob/master/LICENSE)
[![bundle size](http://img.badgesize.io/https://cdn.jsdelivr.net/npm/redoc/bundles/redoc.standalone.js?compression=gzip&max=300000)](https://cdn.jsdelivr.net/npm/redoc/bundles/redoc.standalone.js) [![npm](https://img.shields.io/npm/dm/redoc.svg)](https://www.npmjs.com/package/redoc) [![](https://data.jsdelivr.com/v1/package/npm/redoc/badge)](https://www.jsdelivr.com/package/npm/redoc)
[![Browser Compatibility](https://saucelabs.com/browser-matrix/redoc.svg)](https://saucelabs.com/u/redoc)
![ReDoc demo](demo/redoc-demo.png)
</div>
## [Live demo](http://rebilly.github.io/ReDoc/)
**This is README for `2.0` version of ReDoc (React based). README for `1.x` version is on the branch [v1.x](https://github.com/Rebilly/ReDoc/tree/v1.x)**
![ReDoc demo](https://raw.githubusercontent.com/Rebilly/ReDoc/master/demo/redoc-demo.png)
## [Live demo (v1.x)](http://rebilly.github.io/ReDoc/)
[<img alt="Deploy to Github" src="http://i.imgur.com/YZmaqk3.png" height="60px">](https://github.com/Rebilly/generator-openapi-repo#generator-openapi-repo--) [<img alt="ReDoc as a service" src="http://i.imgur.com/edqdCv6.png" height="60px">](https://redoc.ly) [<img alt="Customization services" src="http://i.imgur.com/c4sUF7M.png" height="60px">](https://redoc.ly/#services)
@ -35,22 +35,28 @@
- Multiple ReDoc instances on single page ([example](demo/examples/multiple-apis/index.html))
## Roadmap
- [ ] [OpenAPI v3.0 support](https://github.com/Rebilly/ReDoc/issues/312)
- [x] ~~[OpenAPI v3.0 support](https://github.com/Rebilly/ReDoc/issues/312)~~
- [x] ~~performance optimizations~~
- [x] ~~better navigation (menu improvements + search)~~
- [x] ~~React rewrite~~
- [x] ~~docs pre-rendering (performance and SEO)~~
- [ ] ability to simple branding/styling
- [ ] built-in API Console
- [ ] docs pre-rendering (performance and SEO)
## Releases
We host the latest and all the previous ReDoc releases on GitHub Pages-based **CDN**:
**Important:** all the 2.x releases are deployed to npm and can be used via unpkg:
- particular release, e.g. `v2.0.0-alpha.15`: https://unpkg.com/redoc@2.0.0-alpha.15/bundles/redoc.standalone.js
- `next` release: https://unpkg.com/redoc@next/bundles/redoc.standalone.js
Additionally, all the 1.x releases are hosted on our GitHub Pages-based **CDN**:
- particular release, e.g. `v1.2.0`: https://rebilly.github.io/ReDoc/releases/v1.2.0/redoc.min.js
- `v1.x.x` release: https://rebilly.github.io/ReDoc/releases/v1.x.x/redoc.min.js
- `latest` release: https://rebilly.github.io/ReDoc/releases/latest/redoc.min.js this file is updated with each release of ReDoc and may introduce breaking changes. **Not recommended to use in production.** Use particular release or `v1.x.x`.
- `latest` release: https://rebilly.github.io/ReDoc/releases/latest/redoc.min.js - it will point to latest 1.x.x release since 2.x releases are not hosted on this CDN but on unpkg.
## Version Guidance
| ReDoc Release | OpenAPI Specification |
|:--------------|:----------------------|
| 2.0.0-alpha.x | 3.0, 2.0 |
| 1.19.x | 2.0 |
| 1.18.x | 2.0 |
| 1.17.x | 2.0 |
@ -75,6 +81,7 @@ We host the latest and all the previous ReDoc releases on GitHub Pages-based **C
<!-- needed for adaptive design -->
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
<!--
ReDoc doesn't change outer page styles
@ -88,7 +95,7 @@ We host the latest and all the previous ReDoc releases on GitHub Pages-based **C
</head>
<body>
<redoc spec-url='http://petstore.swagger.io/v2/swagger.json'></redoc>
<script src="https://rebilly.github.io/ReDoc/releases/latest/redoc.min.js"> </script>
<script src="https://cdn.jsdelivr.net/npm/redoc/bundles/redoc.standalone.js"> </script>
</body>
</html>
```
@ -108,12 +115,12 @@ or using [npm](https://docs.npmjs.com/getting-started/what-is-npm):
### 2. Reference redoc script in HTML
For **CDN**:
```html
<script src="https://rebilly.github.io/ReDoc/releases/latest/redoc.min.js"> </script>
<script src="https://cdn.jsdelivr.net/npm/redoc/bundles/redoc.standalone.js"> </script>
```
For npm:
```html
<script src="node_modules/redoc/dist/redoc.min.js"> </script>
<script src="node_modules/redoc/bundles/redoc.standalone.js"> </script>
```
### 3. Add `<redoc>` element to your page
@ -123,6 +130,45 @@ For npm:
### 4. Enjoy :smile:
## Usage as a React component
Import `RedocStandalone` component from 'redoc' module:
```js
import { RedocStandalone } from 'redoc';
```
and use it somewhere in your component:
```js
<RedocStandalone specUrl="url/to/your/spec"/>
```
or
```js
<RedocStandalone spec={/* spec as an object */}/>
```
Also you can pass options:
```js
<RedocStandalone
specUrl="http://rebilly.github.io/RebillyAPI/swagger.json"
options={{
nativeScrollbars: true,
theme: { colors: { main: '#dd5522' } },
}}
/>
```
Here are detailed [options docs](#redoc-options-object).
## ReDoc CLI
[See here](https://github.com/Rebilly/ReDoc/blob/react-rewrite/cli/README.md)
## Configuration
### Security Definition location
@ -140,50 +186,43 @@ ReDoc makes use of the following [vendor extensions](http://swagger.io/specifica
* [`x-servers`](docs/redoc-vendor-extensions.md#x-servers) - ability to specify different servers for API (backported from OpenAPI 3.0)
* [`x-ignoredHeaderParameters`](docs/redoc-vendor-extensions.md#x-ignoredHeaderParameters) - ability to specify header parameter names to ignore
### `<redoc>` tag attributes
* `spec-url` - relative or absolute url to your spec file;
* `untrusted-spec` - if set, the spec is considered untrusted and all HTML/markdown is sanitized to prevent XSS. **Disabled by default** for performance reasons. **Enable this option if you work with untrusted user data!**
* `scroll-y-offset` - If set, specifies a vertical scroll-offset. This is often useful when there are fixed positioned elements at the top of the page, such as navbars, headers etc;
`scroll-y-offset` can be specified in various ways:
### `<redoc>` options object
You can use all of the following optins with standalone version on <redoc> tag by kebab-casing them, e.g. `scrollYOffset` becomes `scroll-y-offset` and `expandResponses` becomes `expand-responses`.
* `untrustedSpec` - if set, the spec is considered untrusted and all HTML/markdown is sanitized to prevent XSS. **Disabled by default** for performance reasons. **Enable this option if you work with untrusted user data!**
* `scrollYOffset` - If set, specifies a vertical scroll-offset. This is often useful when there are fixed positioned elements at the top of the page, such as navbars, headers etc;
`scrollYOffset` can be specified in various ways:
* **number**: A fixed number of pixels to be used as offset;
* **selector**: selector of the element to be used for specifying the offset. The distance from the top of the page to the element's bottom will be used as offset;
* **function**: A getter function. Must return a number representing the offset (in pixels);
* `suppress-warnings` - if set, warnings are not rendered at the top of documentation (they still are logged to the console).
* `lazy-rendering` - if set, enables lazy rendering mode in ReDoc. This mode is useful for APIs with big number of operations (e.g. > 50). In this mode ReDoc shows initial screen ASAP and then renders the rest operations asynchronously while showing progress bar on the top. Check out the [demo](https://rebilly.github.io/ReDoc) for the example.
* `hide-hostname` - if set, the protocol and hostname is not shown in the operation definition.
* `hide-download-button` - do not show "Download" spec button. **THIS DOESN'T MAKE YOUR SPEC PRIVATE**, it just hides the button.
* `expand-responses` - specify which responses to expand by default by response codes. Values should be passed as comma-separated list without spaces e.g. `expand-responses="200,201"`. Special value `"all"` expands all responses by default. Be careful: this option can slow-down documentation rendering time.
* `required-props-first` - show required properties first ordered in the same order as in `required` array.
* `no-auto-auth` - do not inject Authentication section automatically
* `path-in-middle-panel` - show path link and HTTP verb in the middle panel instead of the right one
* `hide-loading` - do not show loading animation. Useful for small docs
* `native-scrollbars` - use native scrollbar for sidemenu instead of perfect-scroll (scrolling performance optimization for big specs)
* `suppressWarnings` - if set, warnings are not rendered at the top of documentation (they still are logged to the console).
* `lazyRendering` - _Not implemented yet_ ~~if set, enables lazy rendering mode in ReDoc. This mode is useful for APIs with big number of operations (e.g. > 50). In this mode ReDoc shows initial screen ASAP and then renders the rest operations asynchronously while showing progress bar on the top. Check out the [demo](\\rebilly.github.io/ReDoc) for the example.~~
* `hideHostname` - if set, the protocol and hostname is not shown in the operation definition.
* `expandResponses` - specify which responses to expand by default by response codes. Values should be passed as comma-separated list without spaces e.g. `expandResponses="200,201"`. Special value `"all"` expands all responses by default. Be careful: this option can slow-down documentation rendering time.
* `requiredPropsFirst` - show required properties first ordered in the same order as in `required` array.
* `noAutoAuth` - do not inject Authentication section automatically
* `pathInMiddlePanel` - show path link and HTTP verb in the middle panel instead of the right one
* `hideLoading` - do not show loading animation. Useful for small docs
* `nativeScrollbars` - use native scrollbar for sidemenu instead of perfect-scroll (scrolling performance optimization for big specs)
* `hideDownloadButton` - do not show "Download" spec button. **THIS DOESN'T MAKE YOUR SPEC PRIVATE**, it just hides the button.
* `theme` - ReDoc theme. Not documented yet. For details check source code: [theme.ts](https://github.com/Rebilly/ReDoc/blob/react-rewrite/src/theme.ts)
## Advanced usage
## Advanced usage of standalone version
Instead of adding `spec-url` attribute to the `<redoc>` element you can initialize ReDoc via globally exposed `Redoc` object:
```js
Redoc.init(specOrSpecUrl, options)
Redoc.init(specOrSpecUrl, options, element)
```
`specOrSpecUrl` is either JSON object with specification or an URL to the spec in `JSON` or `YAML` format.
`options` is javascript object with camel-cased version of `<redoc>` tag attribute names as the keys, e.g.:
- `specOrSpecUrl` is either JSON object with specification or an URL to the spec in `JSON` or `YAML` format
- `options` [options object](#redoc-options-object)
- `element` DOM element to put ReDoc into
```js
Redoc.init('http://petstore.swagger.io/v2/swagger.json', {
scrollYOffset: 50
})
}, document.getElementById('redoc-container'))
```
-----------
## Development
#### Running local dev-server
- Clone repository
`git clone https://github.com/Rebilly/ReDoc.git`
- Go to the project folder
`cd ReDoc`
- Install dependencies
`npm install`
- _(optional)_ Replace `demo/swagger.yaml` with your own schema
- Start the server
`npm start`
- Open `http://localhost:9000`
Alternatively, Docker can be used by just running `docker-compose up`.
see [CONTRIBUTING.md](.github/CONTRIBUTING.md)

125
benchmark/benchmark.js Normal file
View File

@ -0,0 +1,125 @@
const beautifyBenchmark = require('beautify-benchmark');
const sh = require('shelljs');
const fs = require('fs');
const pathJoin = require('path').join;
const spawn = require('child_process').spawn;
const puppeteer = require('puppeteer');
const args = process.argv.slice(2);
args[0] = args[0] || 'HEAD';
args[1] = args[1] || 'local';
let started = false;
console.log('Benchmarking revisions: ' + args.join(', '));
const localDistDir = './benchmark/revisions/local/bundles';
sh.rm('-rf', localDistDir);
console.log(`Building local dist: ${localDistDir}`);
sh.mkdir('-p', localDistDir);
exec(`yarn bundle:lib --output-path ${localDistDir}`);
const revisions = [];
for (const arg of args) {
revisions.push({ name: arg, path: buildRevisionDist(arg) });
}
const configFile = `
export const revisions = [ ${revisions.map(rev => JSON.stringify(rev)).join(', ')} ];
`;
const configDir = './benchmark/revisions/config.js';
console.log(`Writing config "${configDir}"`);
fs.writeFileSync(configDir, configFile);
console.log('Starging benchmark server');
const proc = spawn('npm', ['run', 'start:benchmark']);
proc.stdout.on('data', data => {
if (data.toString().indexOf('Compiled successfully') > -1) {
console.log('Server started');
startBenchmark();
}
});
proc.stderr.on('data', data => {
console.error(data.toString());
});
proc.on('close', code => {
console.log(`Benchmark server stopped with code ${code}`);
});
async function runPuppeteer() {
return await puppeteer
.launch({ args: ['--no-sandbox', '--disable-setuid-sandbox'] })
.then(async browser => {
const page = await browser.newPage();
let resolve;
const prom = new Promise(_resolve => {
resolve = _resolve;
});
page.on('console', async msg => {
const args = msg.args();
const obj = args.length > 0 && (await args[0].jsonValue());
if (!obj) return;
if (obj.done) {
beautifyBenchmark.log();
// resolve(obj);
} else if (obj.cycle) {
beautifyBenchmark.add(obj.cycle);
} else if (obj.allDone) {
resolve();
} else {
console.log(obj);
}
});
await page.goto('http://127.0.0.1:9090', { timeout: 0 });
const res = await prom;
await browser.close();
return res;
});
}
async function startBenchmark() {
if (started) return;
started = true;
console.log('Starting benchmarks');
await runPuppeteer();
console.log('Killing benchmark server');
proc.kill('SIGINT');
}
function exec(command) {
const { code, stdout, stderr } = sh.exec(command, { silent: true });
if (code !== 0) {
console.error(stdout);
console.error(stderr);
sh.exit(code);
}
return stdout.trim();
}
function buildRevisionDist(revision) {
if (revision === 'local') {
return localDistDir;
}
const hash = exec(`git log -1 --format=%h "${revision}"`);
const buildDir = './benchmark/revisions/' + hash;
const distDir = buildDir + '/bundles';
if (sh.test('-d', distDir)) {
console.log(`Using prebuilt "${revision}"(${hash}) revision: ${buildDir}`);
return distDir;
}
console.log(`Building "${revision}"(${hash}) revision: ${buildDir}`);
sh.mkdir('-p', buildDir);
exec(`git archive "${hash}" | tar -xC "${buildDir}"`);
const pwd = sh.pwd();
sh.cd(buildDir);
exec('yarn remove cypress puppeteer && yarn && yarn bundle:lib');
sh.cd(pwd);
return distDir;
}

28
benchmark/index.html Normal file
View File

@ -0,0 +1,28 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>ReDoc</title>
<style>
body {
margin: 0;
padding: 0;
}
redoc {
display: block;
}
</style>
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
</head>
<body>
<redoc id="example"></redoc>
<!-- <redoc spec-url="./openapi.yaml"></redoc> -->
<script src="https://unpkg.com/lodash@4.17.4/lodash.js"></script>
<script src="https://unpkg.com/benchmark@2.1.4/benchmark.js"></script>
<!-- <script src="../bundles/redoc.standalone.js"></script> -->
</body>
</html>

128
benchmark/index.tsx Normal file
View File

@ -0,0 +1,128 @@
import * as React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { Redoc, RedocProps } from '../src/components';
import { loadAndBundleSpec } from '../src/utils';
import { revisions } from './revisions/config';
import { configure } from 'mobx';
declare var Benchmark;
configure({
isolateGlobalState: true,
});
const node = document.getElementById('example');
const renderRoot = (Component: typeof Redoc, props: RedocProps) =>
render(<Component {...props} />, node!);
async function importRedocs() {
return Promise.all(
revisions.map(rev => {
return import('./' + rev.path.substring(12) + '/redoc.lib.js');
}),
);
}
function startFullTime(redocs, resolvedSpec) {
return new Promise(async resolve => {
const suite = new Benchmark.Suite('Full time', {
maxTime: 20,
initCount: 2,
onStart(event) {
console.log(' ⏱️ ' + event.currentTarget.name);
},
onCycle(event) {
console.log({ cycle: event.target });
},
onComplete() {
console.log({ done: true });
setTimeout(() => resolve(), 10);
},
});
revisions.forEach((rev, idx) => {
const redoc = redocs[idx];
suite.add(rev.name, () => {
const store = new redoc.AppStore(resolvedSpec, 'openapi.yaml');
renderRoot(redoc.Redoc, { store });
unmountComponentAtNode(node!);
});
});
suite.run({ async: true });
});
}
function startInitStore(redocs, resolvedSpec) {
return new Promise(async resolve => {
const suite = new Benchmark.Suite('Create Store Time', {
maxTime: 20,
initCount: 2,
onStart(event) {
console.log(' ⏱️ ' + event.currentTarget.name);
},
onCycle(event) {
console.log({ cycle: event.target });
},
onComplete() {
console.log({ done: true });
setTimeout(() => resolve(), 10);
},
});
revisions.forEach((rev, idx) => {
const redoc = redocs[idx];
suite.add(rev.name, () => {
const store = new redoc.AppStore(resolvedSpec, 'openapi.yaml');
store.dispose();
});
});
suite.run({ async: true });
});
}
function startRenderTime(redocs, resolvedSpec) {
return new Promise(async resolve => {
const suite = new Benchmark.Suite('Render time', {
maxTime: 20,
initCount: 2,
onStart(event) {
console.log(' ⏱️ ' + event.currentTarget.name);
},
onCycle(event) {
console.log({ cycle: event.target });
unmountComponentAtNode(node!);
},
onComplete() {
console.log({ done: true });
setTimeout(() => resolve(), 10);
},
});
revisions.forEach((rev, idx) => {
const redoc = redocs[idx];
const store = new redoc.AppStore(resolvedSpec, 'openapi.yaml');
suite.add(rev.name, () => {
renderRoot(redoc.Redoc, { store });
});
});
suite.run({ async: true });
});
}
async function runBenchmarks() {
const redocs: any[] = await importRedocs();
const resolvedSpec = await loadAndBundleSpec('openapi.yaml');
await startInitStore(redocs, resolvedSpec);
await startRenderTime(redocs, resolvedSpec);
await startFullTime(redocs, resolvedSpec);
console.log({ allDone: true });
}
runBenchmarks();

View File

@ -1,9 +0,0 @@
const path = require('path');
function root(args) {
args = Array.prototype.slice.call(arguments, 0);
return path.join.apply(path, [__dirname, '..'].concat(args));
}
module.exports = {
root: root
}

View File

@ -1,15 +0,0 @@
#!/usr/bin/env node
'use strict';
require('shelljs/global');
set('-e');
set('-v');
cat([
'lib/components/Redoc/redoc-initial-styles.css',
'node_modules/perfect-scrollbar/dist/css/perfect-scrollbar.css',
'node_modules/dropkickjs/build/css/dropkick.css',
'node_modules/prismjs/themes/prism-dark.css',
'node_modules/hint.css/hint.base.css'
]).to('dist/redoc.css')

View File

@ -1,12 +0,0 @@
var path = require('path');
var paths = {
outputName: 'redoc.min.js',
output: 'dist/',
demo: 'demo/**/*',
releases: 'demo/releases/'
}
paths.redocBuilt = path.join(paths.output, paths.outputName);
module.exports = paths;

View File

@ -1,28 +0,0 @@
#!/usr/bin/env node
'use strict';
require('shelljs/global');
var paths = require('./paths');
var path = require('path');
set('-e');
set('-v');
// build
exec('npm run build-dist');
cd('demo');
mv('index-gh.html', 'index.html');
mkdir('-p', 'dist');
cp('-R', '../dist/*', './dist/');
cd('..');
var version = require(path.join(__dirname, '../package.json')).version;
var versionDir = path.join(paths.releases, 'v' + version + '/');
var latestDir = path.join(paths.releases, 'latest/');
var v1Dir = path.join(paths.releases, 'v' + version.split('.')[0] + '.x.x/');
mkdir('-p', versionDir)
mkdir('-p', latestDir);
mkdir('-p', v1Dir);
cp(paths.redocBuilt, versionDir);
cp(paths.redocBuilt, latestDir);
cp(paths.redocBuilt, v1Dir);

View File

@ -1,33 +0,0 @@
#!/usr/bin/env node
'use strict';
require('shelljs/global');
set('-e');
function isPR() {
return process.env.TRAVIS_PULL_REQUEST && process.env.TRAVIS_PULL_REQUEST !== 'false';
}
function isCI() {
return !!process.env.CI;
}
if (process.env.JOB === 'e2e-guru') {
if (isPR()) {
console.log('Skiping E2E tests on PR');
return;
}
exec('npm run e2e');
} else {
exec('npm run unit');
if (isPR()) {
console.log('Skiping E2E tests on PR');
return;
}
if (!isCI()) {
console.log('Skiping E2E tests locally. Use `npm run e2e` to run');
return;
}
console.log('Starting Basic E2E');
exec('npm run e2e');
}

View File

@ -1,136 +0,0 @@
const webpack = require('webpack');
const CheckerPlugin = require('awesome-typescript-loader').CheckerPlugin;
const StringReplacePlugin = require("string-replace-webpack-plugin");
const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');
const VERSION = JSON.stringify(require('../package.json').version);
const root = require('./helpers').root;
module.exports = function (options) {
const conf = {
performance: { hints: false },
output: {
path: root('dist'),
filename: '[name].js',
sourceMapFilename: '[name].[id].map',
chunkFilename: '[id].chunk.js'
},
resolve: {
extensions: ['.ts', '.js', '.json', '.css'],
alias: {
http: 'stream-http',
https: 'https-browserify'
}
},
externals: {
'jquery': 'jquery',
'esprima': 'esprima' // optional dep of ys-yaml not needed for redoc
},
module: {
exprContextCritical: false,
rules: [
{
enforce: 'pre',
test: /\.ts$/,
exclude: [
/node_modules/
],
loader: StringReplacePlugin.replace({
replacements: [
{
pattern: /styleUrls:\s*\[\s*'([\w\.\/-]*)\.css'\s*\][\s,]*$/gm,
replacement: function (match, p1, offset, string) {
return `styleUrls: ['${p1}.scss'],`;
}
},
{
pattern: /(\.\/components\/Redoc\/redoc-initial-styles\.css)/gm,
replacement: function (match, p1, offset, string) {
return p1.replace('.css', '.scss');
}
}
]
})
},
{
enforce: 'pre',
test: /\.js$/,
loader: 'source-map-loader',
exclude: [
/node_modules/
]
},
{
test: /\.json$/,
use: 'json-loader'
},
{
test: /lib[\\\/].*\.css$/,
loaders: ['raw-loader'],
exclude: [/redoc-initial-styles\.css$/]
}, {
test: /\.css$/,
loaders: ['style-loader', 'css-loader?-import'],
exclude: [/lib[\\\/](?!.*redoc-initial-styles).*\.css$/]
},
{
test: /lib[\\\/].*\.scss$/,
loaders: ['raw-loader', 'sass-loader'],
exclude: [/redoc-initial-styles\.scss$/]
},
{
test: /\.scss$/,
use: [
'style-loader',
'css-loader?-import',
'sass-loader'
],
exclude: [/lib[\\\/](?!.*redoc-initial-styles).*\.scss$/]
},
{
test: /\.html$/,
loader: 'raw-loader'
}
],
},
plugins: [
new CheckerPlugin(),
new webpack.DefinePlugin({
'IS_PRODUCTION': options.IS_PRODUCTION,
'LIB_VERSION': VERSION,
'AOT': options.AOT
}),
new StringReplacePlugin()
],
node: {
global: true,
crypto: 'empty',
fs: 'empty',
process: true,
module: false,
clearImmediate: false,
setImmediate: false
}
};
// if (options.AOT) {
// conf.plugins.push(
// new ngcWebpack.NgcWebpackPlugin({
// disable: !options.AOT,
// tsConfig: root('tsconfig.webpack.json'),
// resourceOverride: root('build/resource-override.js')
// })
// );
// }
return conf;
}

View File

@ -1,50 +0,0 @@
const webpack = require('webpack');
const CheckerPlugin = require('awesome-typescript-loader').CheckerPlugin;
const StringReplacePlugin = require("string-replace-webpack-plugin");
const root = require('./helpers').root;
const VERSION = JSON.stringify(require('../package.json').version);
const IS_PRODUCTION = process.env.NODE_ENV === "production";
const webpackMerge = require('webpack-merge'); // used to merge webpack configs
const commonConfig = require('./webpack.common.js');
module.exports = webpackMerge(commonConfig({
IS_PRODUCTION: IS_PRODUCTION,
AOT: IS_PRODUCTION
}), {
devtool: '#inline-source-map',
entry: {
'polyfills': './lib/polyfills.ts',
'redoc': './lib/index.ts',
},
devServer: {
contentBase: root('demo'),
watchContentBase: true,
compress: true,
watchOptions: {
poll: true
},
port: 9000,
hot: false,
stats: 'errors-only'
},
module: {
rules: [
{
test: /\.ts$/,
use: [
'awesome-typescript-loader',
'angular2-template-loader',
],
exclude: [/\.(spec|e2e)\.ts$/]
},
]
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: ['vendor', 'polyfills'],
minChunks: Infinity
})
]
})

View File

@ -1,83 +0,0 @@
const webpack = require('webpack');
const VERSION = JSON.stringify(require('../package.json').version);
const root = require('./helpers').root;
const BANNER =
`ReDoc - OpenAPI/Swagger-generated API Reference Documentation
-------------------------------------------------------------
Version: ${VERSION}
Repo: https://github.com/Rebilly/ReDoc`;
const IS_MODULE = process.env.IS_MODULE != null;
const webpackMerge = require('webpack-merge'); // used to merge webpack configs
const commonConfig = require('./webpack.common.js');
const config = webpackMerge(commonConfig({
IS_PRODUCTION: true,
AOT: true
}), {
devtool: 'source-map',
entry: {
'redoc': IS_MODULE ? ['./lib/redoc.module.ts'] : ['./lib/polyfills.ts', './lib/index.ts']
},
output: {
path: root('dist'),
filename: IS_MODULE ? '[name]-module.js' : '[name].min.js',
sourceMapFilename: IS_MODULE ? '[name]-module.map' : '[name].min.map',
library: 'Redoc',
libraryTarget: 'umd',
umdNamedDefine: true
},
module: {
rules: [
{
test: /\.ts$/,
use: [
'awesome-typescript-loader',
'angular2-template-loader',
],
exclude: [/\.(spec|e2e)\.ts$/]
}
]
},
plugins: [
new webpack.LoaderOptionsPlugin({
minimize: true,
debug: false
}),
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false,
screw_ie8: true
},
mangle: { screw_ie8 : true },
output: {
comments: false
},
sourceMap: true
}),
new webpack.optimize.ModuleConcatenationPlugin(),
new webpack.BannerPlugin(BANNER)
]
})
if (IS_MODULE) {
config.externals = {
'jquery': 'jquery',
'esprima': 'esprima', // optional dep of ys-yaml not needed for redoc
'@angular/platform-browser-dynamic': '@angular/platform-browser-dynamic',
'@angular/platform-browser': '@angular/platform-browser',
'@angular/core': '@angular/core',
'@angular/common': '@angular/common',
'@angular/forms': '@angular/forms',
'core-js': 'core-js',
'rxjs': 'rxjs',
'zone.js/dist/zone': 'zone.js/dist/zone'
};
}
module.exports = config;

View File

@ -1,61 +0,0 @@
const webpack = require('webpack');
const root = require('./helpers').root;
const path = require('path');
const webpackMerge = require('webpack-merge'); // used to merge webpack configs
const commonConfig = require('./webpack.common.js');
module.exports = webpackMerge(commonConfig({
IS_PRODUCTION: true,
AOT: false
}), {
devtool: 'inline-source-map',
module: {
exprContextCritical: false,
rules: [
{
test: /\.ts$/,
use: 'awesome-typescript-loader'
},
{
test: /\.ts$/,
use: [
'angular2-template-loader',
],
exclude: [/\.(spec|e2e)\.ts$/]
},
{
enforce: 'post',
test: /\.(js|ts)$/, loader: 'istanbul-instrumenter-loader',
include: root('lib'),
exclude: [
/\.(e2e|spec)\.ts$/,
/node_modules/
]
}]
},
plugins: [
new webpack.LoaderOptionsPlugin({
test: /\.ts$/,
sourceMap: false,
inlineSourceMap: true,
removeComments: true,
module: "commonjs"
}),
// ignore changes during tests
new webpack.WatchIgnorePlugin([
/[\\\/]ReDoc$/i, // ignore change of ReDoc folder itself
/node_modules[\\\/].*$/,
/\.tmp[\\\/].*$/,
/dist[\\\/].*$/,
/(?:[^\\\/]*(?:[\\\/]|$))*[^\\\/]*\.css$/ // ignore css files
]),
new webpack.ContextReplacementPlugin(
/angular(\\|\/)core(\\|\/)(esm(\\|\/)src|src)(\\|\/)linker/,
path.resolve(__dirname, '../src')
)
],
})

3
cli/.npmignore Normal file
View File

@ -0,0 +1,3 @@
!index.js
!package.json
!README.md

21
cli/README.md Normal file
View File

@ -0,0 +1,21 @@
# redoc-cli
**[ReDoc](https://github.com/Rebilly/ReDoc)'s Command Line Interface**
## Installation
You can use redoc cli by installing `redoc-cli` globally or using [npx](https://medium.com/@maybekatz/introducing-npx-an-npm-package-runner-55f7d4bd282b).
## Usage
Twe following commans are available:
- `redoc-cli serve [spec]` - starts the server with `spec` rendered with ReDoc. Supports SSR mode (`--ssr`) and can watch the spec (`--watch`)
- `redoc-cli bundle [spec]` - bundles spec and ReDoc into **zero-dependency** HTML file.
Some examples:
- Bundle with main color changed to `orange`: <br> `$ redoc-cli bundle [spec] --options.theme.colors.main=orange`
- Serve with `nativeScrollbars` option set to true: <br> `$ redoc-cli serve [spec] --options.nativeScrollbars`
- Bundle using custom template (check [default template](https://github.com/Rebilly/ReDoc/blob/react-rewrite/cli/template.hbs) for reference): <br> `$ redoc-cli bundle [spec] -t custom.hbs`
For more details run `redoc-cli --help`.

290
cli/index.ts Normal file
View File

@ -0,0 +1,290 @@
#!/usr/bin/env node
import * as React from 'react';
import { renderToString } from 'react-dom/server';
import { ServerStyleSheet } from 'styled-components';
import { createServer, ServerResponse, ServerRequest } from 'http';
import * as zlib from 'zlib';
import { join, dirname } from 'path';
import { compile } from 'handlebars';
// @ts-ignore
import { Redoc, loadAndBundleSpec, createStore } from 'redoc';
import { createReadStream, writeFileSync, ReadStream, readFileSync, watch, existsSync } from 'fs';
import * as yargs from 'yargs';
type Options = {
ssr?: boolean;
watch?: boolean;
cdn?: boolean;
output?: string;
title?: string;
templateFileName?: string;
redocOptions?: any;
};
const BUNDLES_DIR = dirname(require.resolve('redoc'));
yargs
.command(
'serve [spec]',
'start the server',
yargs => {
yargs.positional('spec', {
describe: 'path or URL to your spec',
});
yargs.option('s', {
alias: 'ssr',
describe: 'Enable server-side rendering',
type: 'boolean',
});
yargs.option('p', {
alias: 'port',
type: 'number',
default: 8080,
});
yargs.option('w', {
alias: 'watch',
type: 'boolean',
});
yargs.demandOption('spec');
return yargs;
},
async argv => {
try {
await serve(argv.port, argv.spec, {
ssr: argv.ssr,
watch: argv.watch,
templateFileName: argv.template,
redocOptions: argv.options || {},
});
} catch (e) {
console.log(e.stack);
}
},
)
.command(
'bundle [spec]',
'bundle spec into zero-dependency HTML-file',
yargs => {
yargs.positional('spec', {
describe: 'path or URL to your spec',
});
yargs.option('o', {
describe: 'Output file',
alias: 'output',
type: 'string',
default: 'redoc-static.html',
});
yargs.options('title', {
describe: 'Page Title',
type: 'string',
default: 'ReDoc documentation',
});
yargs.option('cdn', {
describe: 'Do not include ReDoc source code into html page, use link to CDN instead',
type: 'boolean',
default: false,
});
yargs.demandOption('spec');
return yargs;
},
async argv => {
try {
await bundle(argv.spec, {
ssr: true,
output: argv.o,
cdn: argv.cdn,
title: argv.title,
templateFileName: argv.template,
redocOptions: argv.options || {},
});
} catch (e) {
console.log(e.message);
}
},
)
.demandCommand()
.options('t', {
alias: 'template',
describe: 'Path to handlebars page template, see https://git.io/vxZ3V for the example ',
type: 'string',
})
.options('options', {
describe: 'ReDoc options, use dot notation, e.g. options.nativeScrollbars',
}).argv;
async function serve(port: number, pathToSpec: string, options: Options = {}) {
let spec = await loadAndBundleSpec(pathToSpec);
let pageHTML = await getPageHTML(spec, pathToSpec, options);
const server = createServer((request, response) => {
console.time('GET ' + request.url);
if (request.url === '/redoc.standalone.js') {
respondWithGzip(
createReadStream(join(BUNDLES_DIR, 'redoc.standalone.js'), 'utf8'),
request,
response,
{
'Content-Type': 'application/javascript',
},
);
} else if (request.url === '/') {
respondWithGzip(pageHTML, request, response);
} else if (request.url === '/spec.json') {
const specStr = JSON.stringify(spec, null, 2);
respondWithGzip(specStr, request, response, {
'Content-Type': 'application/json',
});
} else {
response.writeHead(404);
response.write('Not found');
response.end();
}
console.timeEnd('GET ' + request.url);
});
console.log();
server.listen(port, () => console.log(`Server started: http://127.0.0.1:${port}`));
if (options.watch && existsSync(pathToSpec)) {
watch(
pathToSpec,
debounce(async (event, filename) => {
if (event === 'change' || (event === 'rename' && existsSync(filename))) {
console.log(`${pathToSpec} changed, updating docs`);
try {
spec = await loadAndBundleSpec(pathToSpec);
pageHTML = await getPageHTML(spec, pathToSpec, options);
console.log('Updated successfully');
} catch (e) {
console.error('Error while updating: ', e.message);
}
}
}, 2200),
);
console.log(`👀 Watching ${pathToSpec} for changes...`);
}
}
async function bundle(pathToSpec, options: Options = {}) {
const start = Date.now();
const spec = await loadAndBundleSpec(pathToSpec);
const pageHTML = await getPageHTML(spec, pathToSpec, { ...options, ssr: true });
writeFileSync(options.output!, pageHTML);
const sizeInKiB = Math.ceil(Buffer.byteLength(pageHTML) / 1024);
const time = Date.now() - start;
console.log(
`\n🎉 bundled successfully in: ${options.output!} (${sizeInKiB} KiB) [⏱ ${time / 1000}s]`,
);
}
async function getPageHTML(
spec: any,
pathToSpec: string,
{ ssr, cdn, title, templateFileName, redocOptions = {} }: Options,
) {
let html, css, state;
let redocStandaloneSrc;
if (ssr) {
console.log('Prerendering docs');
const specUrl = redocOptions.specUrl || (isURL(pathToSpec) ? pathToSpec : undefined);
const store = await createStore(spec, specUrl, redocOptions);
const sheet = new ServerStyleSheet();
html = renderToString(sheet.collectStyles(React.createElement(Redoc, { store })));
css = sheet.getStyleTags();
state = await store.toJS();
if (!cdn) {
redocStandaloneSrc = readFileSync(join(BUNDLES_DIR, 'redoc.standalone.js'));
}
}
templateFileName = templateFileName ? templateFileName : join(__dirname, './template.hbs');
const template = compile(readFileSync(templateFileName).toString());
return template({
redocHTML: `
<script>
${(ssr && `const __redoc_state = ${JSON.stringify(state)};`) || ''}
document.addEventListener('DOMContentLoaded', function() {
var container = document.getElementById('redoc');
Redoc.${
ssr
? 'hydrate(__redoc_state, container);'
: `init("spec.json", ${JSON.stringify(redocOptions)}, container)`
};
});
</script>
<div id="redoc">${(ssr && html) || ''}</div>`,
redocHead: ssr
? (cdn
? '<script src="https://unpkg.com/redoc@next/bundles/redoc.standalone.js"></script>'
: `<script>${redocStandaloneSrc}</script>`) + css
: '<script src="redoc.standalone.js"></script>',
title: title,
});
}
// credits: https://stackoverflow.com/a/9238214/1749888
function respondWithGzip(
contents: string | ReadStream,
request: ServerRequest,
response: ServerResponse,
headers = {},
) {
let compressedStream;
const acceptEncoding = (request.headers['accept-encoding'] as string) || '';
if (acceptEncoding.match(/\bdeflate\b/)) {
response.writeHead(200, { ...headers, 'content-encoding': 'deflate' });
compressedStream = zlib.createDeflate();
} else if (acceptEncoding.match(/\bgzip\b/)) {
response.writeHead(200, { ...headers, 'content-encoding': 'gzip' });
compressedStream = zlib.createGzip();
} else {
response.writeHead(200, headers);
if (typeof contents === 'string') {
response.write(contents);
response.end();
} else {
contents.pipe(response);
}
return;
}
if (typeof contents === 'string') {
compressedStream.write(contents);
compressedStream.pipe(response);
compressedStream.end();
return;
} else {
contents.pipe(compressedStream).pipe(response);
}
}
function debounce(callback: Function, time: number) {
let interval;
return (...args) => {
clearTimeout(interval);
interval = setTimeout(() => {
interval = null;
callback(...args);
}, time);
};
}
function isURL(str: string): boolean {
return /^(https?:)\/\//m.test(str);
}

28
cli/package.json Normal file
View File

@ -0,0 +1,28 @@
{
"name": "redoc-cli",
"version": "0.3.5",
"description": "ReDoc's Command Line Interface",
"main": "index.js",
"bin": "index.js",
"repository": "https://github.com/Rebilly/ReDoc",
"author": "Roman Hotsiy <gotsijroman@gmail.com>",
"license": "MIT",
"dependencies": {
"handlebars": "^4.0.11",
"isarray": "^2.0.4",
"react": "^16.3.0-alpha.2",
"react-dom": "^16.3.0-alpha.2",
"redoc": "^2.0.0-alpha.15",
"yargs": "^11.0.0"
},
"scripts": {
"ci-publish": "ci-publish"
},
"publishConfig": {
"access": "public"
},
"devDependencies": {
"@types/handlebars": "^4.0.36",
"ci-publish": "^1.3.1"
}
}

469
cli/redoc-static.html Normal file

File diff suppressed because one or more lines are too long

23
cli/template.hbs Normal file
View File

@ -0,0 +1,23 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf8" />
<title>{{title}}</title>
<!-- needed for adaptive design -->
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {
padding: 0;
margin: 0;
}
</style>
{{{redocHead}}}
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
</head>
<body>
{{{redocHTML}}}
</body>
</html>

2853
cli/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

30
custom.d.ts vendored
View File

@ -1,25 +1,25 @@
declare module "*.css" {
declare module '*.json' {
const content: any;
export = content;
}
declare module '*.svg' {
const content: string;
export default content;
}
declare module "*.json" {
declare module '*.css' {
const content: string;
export default content;
}
declare var LIB_VERSION: any;
declare var IS_PRODUCTION: any;
declare var AOT: any;
declare var __REDOC_VERSION__: string;
declare var __REDOC_REVISION__: string;
interface ErrorStackTraceLimit {
stackTraceLimit: number;
declare type Dict<T> = {
[key: string]: T;
};
interface Element {
scrollIntoViewIfNeeded(centerIfNeeded?: boolean): void;
}
interface History {
scrollRestoration: "auto"|"manual";
}
interface Window {
HTMLElement: any
}
declare var safari: any;
interface ErrorConstructor extends ErrorStackTraceLimit {}

11
cypress.json Normal file
View File

@ -0,0 +1,11 @@
{
"ignoreTestFiles": "*.js.map",
"integrationFolder": "e2e/integration",
"pluginsFile": "e2e/plugins/index.js",
"fixturesFolder": false,
"supportFile": false,
"fileServerFolder": ".",
"videoRecording": true,
"screenshotOnHeadlessFailure": true,
"projectId": "z6eb6h"
}

220
demo/ComboBox.tsx Normal file
View File

@ -0,0 +1,220 @@
/**
* Could not find ready-to-use component with required behaviour so
* I quickly hacked my own. Will refactor into separate npm package later
*/
import * as React from 'react';
import styled, { StyledFunction } from 'styled-components';
function withProps<T, U extends HTMLElement = HTMLElement>(
styledFunction: StyledFunction<React.HTMLProps<U>>,
): StyledFunction<T & React.HTMLProps<U>> {
return styledFunction;
}
const DropDownItem = withProps<{ active: boolean }>(styled.li)`
${props => ((props as any).active ? 'background-color: #eee' : '')};
padding: 13px 16px;
&:hover {
background-color: #eee;
}
cursor: pointer;
text-overflow: ellipsis;
overflow: hidden;
`;
const DropDownList = styled.ul`
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12),
0 3px 1px -2px rgba(0, 0, 0, 0.2);
background: #fff;
border-radius: 0 0 2px 2px;
top: 100%;
left: 0;
right: 0;
z-index: 200;
overflow: hidden;
position: absolute;
list-style: none;
margin: 4px 0 0 0;
padding: 5px 0;
font-family: 'Lato';
overflow: hidden;
`;
const ComboBoxWrap = styled.div`
position: relative;
width: 100%;
max-width: 500px;
display: flex;
`;
const Input = styled.input`
box-sizing: border-box;
width: 100%;
padding: 0 10px;
color: #555;
background-color: #fff;
border: 1px solid #ccc;
font-size: 16px;
height: 28px;
box-sizing: border-box;
vertical-align: middle;
line-height: 1;
outline: none;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
&:focus {
border-color: #66afe9;
outline: 0;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6);
}
`;
const Button = styled.button`
background-color: #fff;
color: #333;
padding: 2px 10px;
touch-action: manipulation;
cursor: pointer;
user-select: none;
border: 1px solid #ccc;
border-left: 0;
font-size: 16px;
height: 28px;
box-sizing: border-box;
vertical-align: middle;
line-height: 1;
outline: none;
width: 80px;
`;
export interface ComboBoxProps {
onChange?: (val: string) => void;
options: Array<{ value: string; label: string }>;
placeholder?: string;
value?: string;
}
export interface ComboBoxState {
open: boolean;
value: string;
activeItemIdx: number;
}
export default class ComboBox extends React.Component<ComboBoxProps, ComboBoxState> {
state = {
open: false,
value: this.props.value || '',
activeItemIdx: -1,
};
open = () => {
this.setState({
open: true,
});
};
close = () => {
this.setState({
open: false,
activeItemIdx: -1,
});
};
handleChange = e => {
this.updateValue(e.currentTarget.value);
};
updateValue(value) {
this.setState({
value,
activeItemIdx: -1,
});
}
handleSelect(value: string) {
this.updateValue(value);
if (this.props.onChange) {
this.props.onChange(value);
}
this.close();
}
handleTryItClick = () => {
this.handleSelect(this.state.value);
};
handleKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.keyCode === 13) {
this.handleSelect(e.currentTarget.value);
} else if (e.keyCode === 40) {
const activeItemIdx = Math.min(this.props.options.length - 1, ++this.state.activeItemIdx);
this.setState({
open: true,
activeItemIdx,
value: this.props.options[activeItemIdx].value,
});
e.preventDefault();
} else if (e.keyCode === 38) {
const activeItemIdx = Math.max(0, --this.state.activeItemIdx);
this.setState({
activeItemIdx,
value: this.props.options[activeItemIdx].value,
});
e.preventDefault();
} else if (e.keyCode === 27) {
this.close();
}
};
handleBlur = () => {
setTimeout(() => this.close(), 100);
};
handleItemClick = (val, idx) => {
this.handleSelect(val);
this.setState({
activeItemIdx: idx,
});
};
renderOption = (option: { value: string; label: string }, idx: number) => {
return (
<DropDownItem
active={idx === this.state.activeItemIdx}
key={option.value}
// tslint:disable-next-line
onMouseDown={() => {
this.handleItemClick(option.value, idx);
}}
>
<small>
<strong>{option.label}</strong>
</small>
<br />
{option.value}
</DropDownItem>
);
};
render() {
const { open, value } = this.state;
const { options, placeholder } = this.props;
return (
<ComboBoxWrap>
<Input
placeholder={placeholder}
onChange={this.handleChange}
value={value}
onFocus={this.open}
onBlur={this.handleBlur}
onKeyDown={this.handleKeyPress}
/>
<Button onClick={this.handleTryItClick}> TRY IT </Button>
{open && <DropDownList>{options.map(this.renderOption)}</DropDownList>}
</ComboBoxWrap>
);
}
}

27362
demo/big-openapi.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,79 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>ReDoc Demo: Multiple apis</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {
margin: 0;
padding-top: 40px;
}
nav {
position: fixed;
top: 0;
width: 100%;
z-index: 100;
}
#links_container {
margin: 0;
padding: 0;
background-color: #0033a0;
}
#links_container li {
display: inline-block;
padding: 10px;
color: white;
cursor: pointer;
}
</style>
</head>
<body>
<!-- Top navigation placeholder -->
<nav>
<ul id="links_container">
</ul>
</nav>
<redoc scroll-y-offset="body > nav"></redoc>
<script src="https://rebilly.github.io/ReDoc/releases/v1.x.x/redoc.min.js"> </script>
<script>
// list of APIS
var apis = [
{
name: 'PetStore',
url: 'https://rebilly.github.io/ReDoc/swagger.yaml'
},
{
name: 'Instagram',
url: 'https://api.apis.guru/v2/specs/instagram.com/1.0.0/swagger.yaml'
},
{
name: 'Google Calendar',
url: 'https://api.apis.guru/v2/specs/googleapis.com/calendar/v3/swagger.yaml'
}
];
// initially render first API
Redoc.init(apis[0].url);
function onClick() {
var url = this.getAttribute('data-link');
Redoc.init(url);
}
// dynamically building navigation items
var $list = document.getElementById('links_container');
apis.forEach(function(api) {
var $listitem = document.createElement('li');
$listitem.setAttribute('data-link', api.url);
$listitem.innerText = api.name;
$listitem.addEventListener('click', onClick);
$list.appendChild($listitem);
});
</script>
</body>
</html>

View File

@ -1,39 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>ReDoc</title>
<link rel="stylesheet" href="main.css">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://cdn.vaadin.com/vaadin-core-elements/1.2.0/webcomponentsjs/webcomponents-lite.min.js"></script>
<link rel="import" href="https://cdn.vaadin.com/vaadin-core-elements/1.2.0/vaadin-combo-box/vaadin-combo-box-light.html">
</head>
<body>
<nav>
<header> <a href="/ReDoc"> ReDoc </a> </header>
<template is="dom-bind" id="specs">
<form id="schema-url-form" is="iron-form">
<vaadin-combo-box-light id="spec-input" items="[[specs]]" allow-custom-value>
<input placeholder="URL to a spec to try" id="schema-url-input" type="text" is="iron-input" value="https://rebilly.github.io/RebillyAPI/swagger.json">
</vaadin-combo-box-light>
<button type="submit"> Explore </button>
</form>
</template>
<iframe src="https://ghbtns.com/github-btn.html?user=Rebilly&repo=ReDoc&type=star&count=true&size=large"
frameborder="0" scrolling="0" width="150px" height="30px"></iframe>
</nav>
<redoc scroll-y-offset="body > nav" spec-url='swagger.yaml' lazy-rendering untrusted-spec></redoc>
<script src="main.js"> </script>
<script src="./dist/redoc.min.js"> </script>
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-81703547-1', 'auto');
ga('send', 'pageview');
</script>
</body>
</html>

View File

@ -1,35 +1,40 @@
<!DOCTYPE html>
<html>
<head>
<title>ReDoc</title>
<link rel="stylesheet" href="main.css">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://cdn.vaadin.com/vaadin-core-elements/1.2.0/webcomponentsjs/webcomponents-lite.min.js"></script>
<link rel="import" href="https://cdn.vaadin.com/vaadin-core-elements/1.2.0/vaadin-combo-box/vaadin-combo-box-light.html">
</head>
<body>
<nav>
<header> <a href="/ReDoc"> ReDoc </a> </header>
<template is="dom-bind" id="specs">
<form id="schema-url-form" is="iron-form">
<vaadin-combo-box-light id="spec-input" items="[[specs]]" allow-custom-value>
<input placeholder="URL to a spec to try" id="schema-url-input" type="text" is="iron-input" value="https://rebilly.github.io/RebillyAPI/swagger.json">
</vaadin-combo-box-light>
<button type="submit"> Explore </button>
</form>
</template>
<iframe src="https://ghbtns.com/github-btn.html?user=Rebilly&repo=ReDoc&type=star&count=true&size=large"
frameborder="0" scrolling="0" width="150px" height="30px"></iframe>
</nav>
<redoc scroll-y-offset="body > nav" spec-url='swagger.yaml' lazy-rendering untrusted-spec></redoc>
<head>
<meta charset="UTF-8" />
<title>ReDoc Interactive Demo</title>
<meta name="description" content="ReDoc Interactive Demo. OpenAPI/Swagger-generated API Reference Documentation" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {
margin: 0;
padding: 0;
}
<script>
window.__REDOC_DEV__ = true;
</script>
<script src="main.js"> </script>
<script src="/webpack-dev-server.js"></script>
<script src="/polyfills.js"></script>
<script src="/redoc.js"></script>
</body>
</html>
redoc {
display: block;
}
</style>
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
</head>
<body>
<div id="container"> </div>
<script>
(function (i, s, o, g, r, a, m) {
i['GoogleAnalyticsObject'] = r; i[r] = i[r] || function () {
(i[r].q = i[r].q || []).push(arguments)
}, i[r].l = 1 * new Date(); a = s.createElement(o),
m = s.getElementsByTagName(o)[0]; a.async = 1; a.src = g; m.parentNode.insertBefore(a, m)
})(window, document, 'script', 'https://www.google-analytics.com/analytics.js', 'ga');
if (window.location.host === 'rebilly.github.io') {
ga('create', 'UA-81703547-1', 'auto');
ga('send', 'pageview');
}
</script>
</body>
</html>

182
demo/index.tsx Normal file
View File

@ -0,0 +1,182 @@
import * as React from 'react';
import { render } from 'react-dom';
import styled from 'styled-components';
import { resolve as urlResolve } from 'url';
import { RedocStandalone } from '../src';
import ComboBox from './ComboBox';
const demos = [
{ value: 'https://api.apis.guru/v2/specs/instagram.com/1.0.0/swagger.yaml', label: 'Instagram' },
{
value: 'https://api.apis.guru/v2/specs/googleapis.com/calendar/v3/swagger.yaml',
label: 'Google Calendar',
},
{ value: 'https://api.apis.guru/v2/specs/slack.com/1.0.3/swagger.yaml', label: 'Slack' },
{ value: 'https://api.apis.guru/v2/specs/zoom.us/2.0.0/swagger.yaml', label: 'Zoom.us' },
{
value: 'https://api.apis.guru/v2/specs/graphhopper.com/1.0/swagger.yaml',
label: 'GraphHopper',
},
];
const DEFAULT_SPEC = 'openapi.yaml';
class DemoApp extends React.Component<
{},
{ specUrl: string; dropdownOpen: boolean; cors: boolean }
> {
constructor(props) {
super(props);
let parts = window.location.search.match(/url=([^&]+)/);
let url = DEFAULT_SPEC;
if (parts && parts.length > 1) {
url = decodeURIComponent(parts[1]);
}
parts = window.location.search.match(/[?&]nocors(&|#|$)/);
let cors = true;
if (parts && parts.length > 1) {
cors = false;
}
this.state = {
specUrl: url,
dropdownOpen: false,
cors,
};
}
handleChange = (url: string) => {
this.setState({
specUrl: url,
});
window.history.pushState(
undefined,
'',
updateQueryStringParameter(location.search, 'url', url),
);
};
toggleCors = (e: React.ChangeEvent<HTMLInputElement>) => {
const cors = e.currentTarget.checked;
this.setState({
cors,
});
window.history.pushState(
undefined,
'',
updateQueryStringParameter(location.search, 'nocors', cors ? undefined : ''),
);
};
render() {
const { specUrl, cors } = this.state;
let proxiedUrl = specUrl;
if (specUrl !== DEFAULT_SPEC) {
proxiedUrl = cors
? '\\\\cors.apis.guru/' + urlResolve(window.location.href, specUrl)
: specUrl;
}
return (
<>
<Heading>
<a href=".">
<Logo src="https://github.com/Rebilly/ReDoc/raw/master/docs/images/redoc-logo.png" />
</a>
<ControlsContainer>
<ComboBox
placeholder={'URL to a spec to try'}
options={demos}
onChange={this.handleChange}
value={specUrl === DEFAULT_SPEC ? '' : specUrl}
/>
<CorsCheckbox title="Use CORS proxy">
<input id="cors_checkbox" type="checkbox" onChange={this.toggleCors} checked={cors} />
<label htmlFor="cors_checkbox">CORS</label>
</CorsCheckbox>
</ControlsContainer>
<iframe
src="https://ghbtns.com/github-btn.html?user=Rebilly&amp;repo=ReDoc&amp;type=star&amp;count=true&amp;size=large"
frameBorder="0"
scrolling="0"
width="150px"
height="30px"
/>
</Heading>
<RedocStandalone specUrl={proxiedUrl} options={{ scrollYOffset: 'nav' }} />
</>
);
}
}
/* ====== Styled components ====== */
const ControlsContainer = styled.div`
display: flex;
justify-content: center;
flex: 1;
margin: 0 15px;
align-items: center;
`;
const CorsCheckbox = styled.div`
margin-left: 10px;
label {
font-size: 13px;
}
`;
const Heading = styled.nav`
position: sticky;
top: 0;
height: 50px;
box-sizing: border-box;
background: white;
border-bottom: 1px solid #cccccc;
z-index: 10;
padding: 5px;
display: flex;
align-items: center;
font-family: 'Lato';
`;
const Logo = styled.img`
height: 40px;
width: 124px;
display: inline-block;
margin-right: 15px;
`;
render(<DemoApp />, document.getElementById('container'));
/* ====== Helpers ====== */
function updateQueryStringParameter(uri, key, value) {
const keyValue = value === '' ? key : key + '=' + value;
const re = new RegExp('([?|&])' + key + '=?.*?(&|#|$)', 'i');
if (uri.match(re)) {
if (value !== undefined) {
return uri.replace(re, '$1' + keyValue + '$2');
} else {
return uri.replace(re, (_, separator: string, rest: string) => {
if (rest.startsWith('&')) {
rest = rest.substring(1);
}
return separator === '&' ? rest : separator + rest;
});
}
} else {
if (value === undefined) {
return uri;
}
let hash = '';
if (uri.indexOf('#') !== -1) {
hash = uri.replace(/.*#/, '#');
uri = uri.replace(/#.*/, '');
}
const separator = uri.indexOf('?') !== -1 ? '&' : '?';
return uri + separator + keyValue + hash;
}
}

View File

@ -1,133 +0,0 @@
body {
margin: 0;
padding-top: 50px;
-webkit-tap-highlight-color: rgba(0,0,0,0);
-moz-tap-highlight-color: rgba(0,0,0,0);
-ms-tap-highlight-color: rgba(0,0,0,0);
-o-tap-highlight-color: rgba(0,0,0,0);
tap-highlight-color: rgba(0,0,0,0);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-smoothing: antialiased;
-webkit-osx-font-smoothing: grayscale;
-moz-osx-font-smoothing: grayscale;
osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
-moz-text-size-adjust: 100%;
text-size-adjust: 100%;
-webkit-text-shadow: 1px 1px 1px rgba(0,0,0,0.004);
-ms-text-shadow: 1px 1px 1px rgba(0,0,0,0.004);
text-shadow: 1px 1px 1px rgba(0,0,0,0.004);
text-rendering: optimizeSpeed!important;
font-smooth: always;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
text-size-adjust: 100%;
font-family: Monserrat, sans-serif;
}
nav input, nav button {
font-size: 16px;
height: 28px;
box-sizing: border-box;
vertical-align: middle;
line-height: 1;
outline: none;
}
nav header {
font-family: Monserrat, sans-serif;
float: left;
margin-left: 20px;
font-size: 25px;
color: #00329F;
font-weight: bold;
}
nav input {
width: 50%;
box-sizing: border-box;
max-width: 500px;
padding: 0 10px;
color: #555;
background-color: #fff;
border: 1px solid #ccc;
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075);
box-shadow: inset 0 1px 1px rgba(0,0,0,.075);
-webkit-transition: border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;
-o-transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s;
transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s;
}
nav input:focus {
border-color: #66afe9;
outline: 0;
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);
box-shadow: inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);
}
nav button {
background-color: #fff;
color: #333;
padding: 2px 10px;
touch-action: manipulation;
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
border: 1px solid #ccc;
}
nav button:hover {
background-color: #e6e6e6;
border-color: #adadad;
}
nav button:active {
background-color: #d4d4d4;
border-color: #8c8c8c;
}
nav {
width: 100%;
height: 50px;
line-height: 50px;
text-align: center;
background-color: white;
border-bottom: 1px solid #ccc;
position: fixed;
top: 0;
z-index: 1;
box-sizing: border-box;
}
nav iframe {
margin: 10px 0;
position: absolute;
right: 0;
top: 0;
}
header a {
color: inherit;
text-decoration: none;
}
@media (min-width: 1000px) {
nav header {
position: absolute;
}
}
@media (max-width: 500px) {
nav input {
width: 70%;
}
nav header {
display: none;
}
nav iframe {
display: none;
}
}

View File

@ -1,74 +0,0 @@
;(function() {
'use strict';
var schemaUrlForm = document.getElementById('schema-url-form');
var schemaUrlInput;
var url = window.location.search.match(/url=([^&]+)/);
if (url && url.length > 1) {
url = decodeURIComponent(url[1]);
url = window.__REDOC_DEV__ ? url : '\\\\cors.apis.guru/' + url;
document.getElementsByTagName('redoc')[0].setAttribute('spec-url', url);
}
function updateQueryStringParameter(uri, key, value) {
var re = new RegExp("([?|&])" + key + "=.*?(&|#|$)", "i");
if (uri.match(re)) {
return uri.replace(re, '$1' + key + "=" + value + '$2');
} else {
var hash = '';
if( uri.indexOf('#') !== -1 ){
hash = uri.replace(/.*#/, '#');
uri = uri.replace(/#.*/, '');
}
var separator = uri.indexOf('?') !== -1 ? "&" : "?";
return uri + separator + key + "=" + value + hash;
}
}
var specs = document.querySelector('#specs');
specs.addEventListener('dom-change', function() {
schemaUrlForm = document.getElementById('schema-url-form');
schemaUrlInput = document.getElementById('schema-url-input');
schemaUrlForm.addEventListener('submit', function(event) {
event.preventDefault();
event.stopPropagation();
location.search = updateQueryStringParameter(location.search, 'url', schemaUrlInput.value)
return false;
})
schemaUrlInput.addEventListener('mousedown', function(e) {
e.stopPropagation();
});
schemaUrlInput.value = url;
specs.specs = [
'https://api.apis.guru/v2/specs/instagram.com/1.0.0/swagger.yaml',
'https://api.apis.guru/v2/specs/googleapis.com/calendar/v3/swagger.yaml',
'https://api.apis.guru/v2/specs/data2crm.com/1/swagger.yaml',
'https://api.apis.guru/v2/specs/graphhopper.com/1.0/swagger.yaml'
];
var $specInput = document.getElementById('spec-input');
$specInput.addEventListener('value-changed', function(e) {
schemaUrlInput.value = e.detail.value;
location.search = updateQueryStringParameter(location.search, 'url', schemaUrlInput.value);
});
function selectItem() {
let value = this.innerText.trim();
schemaUrlInput.value = value;
location.search = updateQueryStringParameter(location.search, 'url', schemaUrlInput.value);
}
// for some reason events are not triggered so have to dirty fix this
$specInput.addEventListener('click', function(event) {
let $elems = document.querySelectorAll('.item.vaadin-combo-box-overlay');
$elems.forEach(function($el) {
$el.addEventListener('mousedown', selectItem);
$el.addEventListener('mousedown', selectItem);
});
});
});
})();

949
demo/openapi.yaml Normal file
View File

@ -0,0 +1,949 @@
openapi: 3.0.0
servers:
- url: //petstore.swagger.io/v2
description: Default server
- url: //petstore.swagger.io/sandbox
description: Sandbox server
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.
# Introduction
This API is documented in **OpenAPI format** and is based on
[Petstore sample](http://petstore.swagger.io/) provided by
[swagger.io](http://swagger.io) team.
It was **extended** to illustrate features of
[generator-openapi-repo](https://github.com/Rebilly/generator-openapi-repo)
tool and [ReDoc](https://github.com/Rebilly/ReDoc) documentation. In
addition to standard
OpenAPI syntax we use a few [vendor
extensions](https://github.com/Rebilly/ReDoc/blob/master/docs/redoc-vendor-extensions.md).
# OpenAPI Specification
This API is documented in **OpenAPI format** and is based on
[Petstore sample](http://petstore.swagger.io/) provided by
[swagger.io](http://swagger.io) team.
It was **extended** to illustrate features of
[generator-openapi-repo](https://github.com/Rebilly/generator-openapi-repo)
tool and [ReDoc](https://github.com/Rebilly/ReDoc) documentation. In
addition to standard
OpenAPI syntax we use a few [vendor
extensions](https://github.com/Rebilly/ReDoc/blob/master/docs/redoc-vendor-extensions.md).
# Cross-Origin Resource Sharing
This API features Cross-Origin Resource Sharing (CORS) implemented in
compliance with [W3C spec](https://www.w3.org/TR/cors/).
And that allows cross-domain communication from the browser.
All responses have a wildcard same-origin which makes them completely public
and accessible to everyone, including any code on any site.
# Authentication
Petstore offers two forms of authentication:
- API Key
- OAuth2
OAuth2 - an open protocol to allow secure authorization in a simple
and standard method from web, mobile and desktop applications.
<!-- ReDoc-Inject: <security-definitions> -->
version: 1.0.0
title: Swagger Petstore
termsOfService: 'http://swagger.io/terms/'
contact:
name: API Support
email: apiteam@swagger.io
url: https://github.com/Rebilly/ReDoc
x-logo:
url: 'https://rebilly.github.io/ReDoc/petstore-logo.png'
license:
name: Apache 2.0
url: 'http://www.apache.org/licenses/LICENSE-2.0.html'
externalDocs:
description: Find out how to create Github repo for your OpenAPI spec.
url: 'https://github.com/Rebilly/generator-openapi-repo'
tags:
- name: pet
description: Everything about your Pets
- name: store
description: Access to Petstore orders
- name: user
description: Operations about user
x-tagGroups:
- name: General
tags:
- pet
- store
- name: User Management
tags:
- user
paths:
/pet:
parameters:
- name: cookieParam
in: cookie
description: Some cookie
required: true
schema:
type: integer
format: int64
post:
tags:
- pet
summary: Add a new pet to the store
description: Add new pet to the store inventory.
operationId: addPet
responses:
'405':
description: Invalid input
security:
- petstore_auth:
- 'write:pets'
- 'read:pets'
x-code-samples:
- lang: 'C#'
source: |
PetStore.v1.Pet pet = new PetStore.v1.Pet();
pet.setApiKey("your api key");
pet.petType = PetStore.v1.Pet.TYPE_DOG;
pet.name = "Rex";
// set other fields
PetStoreResponse response = pet.create();
if (response.statusCode == HttpStatusCode.Created)
{
// Successfully created
}
else
{
// Something wrong -- check response for errors
Console.WriteLine(response.getRawResponse());
}
- lang: PHP
source: |
$form = new \PetStore\Entities\Pet();
$form->setPetType("Dog");
$form->setName("Rex");
// set other fields
try {
$pet = $client->pets()->create($form);
} catch (UnprocessableEntityException $e) {
var_dump($e->getErrors());
}
requestBody:
$ref: '#/components/requestBodies/Pet'
put:
tags:
- pet
summary: Update an existing pet
description: ''
operationId: updatePet
responses:
'400':
description: Invalid ID supplied
'404':
description: Pet not found
'405':
description: Validation exception
security:
- petstore_auth:
- 'write:pets'
- 'read:pets'
x-code-samples:
- lang: PHP
source: |
$form = new \PetStore\Entities\Pet();
$form->setPetId(1);
$form->setPetType("Dog");
$form->setName("Rex");
// set other fields
try {
$pet = $client->pets()->update($form);
} catch (UnprocessableEntityException $e) {
var_dump($e->getErrors());
}
requestBody:
$ref: '#/components/requestBodies/Pet'
'/pet/{petId}':
get:
tags:
- pet
summary: Find pet by ID
description: Returns a single pet
operationId: getPetById
parameters:
- name: petId
in: path
description: ID of pet to return
required: true
deprecated: true
schema:
type: integer
format: int64
responses:
'200':
description: successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/Pet'
application/xml:
schema:
$ref: '#/components/schemas/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
parameters:
- name: petId
in: path
description: ID of pet that needs to be updated
required: true
schema:
type: integer
format: int64
responses:
'405':
description: Invalid input
security:
- petstore_auth:
- 'write:pets'
- 'read:pets'
requestBody:
content:
application/x-www-form-urlencoded:
schema:
type: object
properties:
name:
description: Updated name of the pet
type: string
status:
description: Updated status of the pet
type: string
delete:
tags:
- pet
summary: Deletes a pet
description: ''
operationId: deletePet
parameters:
- name: api_key
in: header
required: false
schema:
type: string
example: "Bearer <TOKEN>"
- name: petId
in: path
description: Pet id to delete
required: true
schema:
type: integer
format: int64
responses:
'400':
description: Invalid pet value
security:
- petstore_auth:
- 'write:pets'
- 'read:pets'
'/pet/{petId}/uploadImage':
post:
tags:
- pet
summary: uploads an image
description: ''
operationId: uploadFile
parameters:
- name: petId
in: path
description: ID of pet to update
required: true
schema:
type: integer
format: int64
responses:
'200':
description: successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
security:
- petstore_auth:
- 'write:pets'
- 'read:pets'
requestBody:
content:
application/octet-stream:
schema:
type: string
format: binary
/pet/findByStatus:
get:
tags:
- pet
summary: Finds Pets by status
description: Multiple status values can be provided with comma seperated strings
operationId: findPetsByStatus
parameters:
- name: status
in: query
description: Status values that need to be considered for filter
required: true
style: form
schema:
type: array
items:
type: string
enum:
- available
- pending
- sold
default: available
responses:
'200':
description: successful operation
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Pet'
application/xml:
schema:
type: array
items:
$ref: '#/components/schemas/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 seperated strings. Use tag1,
tag2, tag3 for testing.
operationId: findPetsByTags
deprecated: true
parameters:
- name: tags
in: query
description: Tags to filter by
required: true
style: form
schema:
type: array
items:
type: string
responses:
'200':
description: successful operation
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Pet'
application/xml:
schema:
type: array
items:
$ref: '#/components/schemas/Pet'
'400':
description: Invalid tag value
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
responses:
'200':
description: successful operation
content:
application/json:
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
responses:
'200':
description: successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/Order'
application/xml:
schema:
$ref: '#/components/schemas/Order'
'400':
description: Invalid Order
content:
application/json:
example:
status: 400
message: "Invalid Order"
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Order'
description: order placed for purchasing the pet
required: true
'/store/order/{orderId}':
get:
tags:
- store
summary: Find purchase order by ID
description: >-
For valid response try integer IDs with value <= 5 or > 10. Other values
will generated exceptions
operationId: getOrderById
parameters:
- name: orderId
in: path
description: ID of pet that needs to be fetched
required: true
schema:
type: integer
format: int64
minimum: 1
maximum: 5
responses:
'200':
description: successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/Order'
application/xml:
schema:
$ref: '#/components/schemas/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 value < 1000. Anything above
1000 or nonintegers will generate API errors
operationId: deleteOrder
parameters:
- name: orderId
in: path
description: ID of the order that needs to be deleted
required: true
schema:
type: string
minimum: 1
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
responses:
default:
description: successful operation
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/User'
description: Created user object
required: true
'/user/{username}':
get:
tags:
- user
summary: Get user by user name
description: ''
operationId: getUserByName
parameters:
- name: username
in: path
description: 'The name that needs to be fetched. Use user1 for testing. '
required: true
schema:
type: string
responses:
'200':
description: successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/User'
application/xml:
schema:
$ref: '#/components/schemas/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
parameters:
- name: username
in: path
description: name that need to be deleted
required: true
schema:
type: string
responses:
'400':
description: Invalid user supplied
'404':
description: User not found
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/User'
description: Updated user object
required: true
delete:
tags:
- user
summary: Delete user
description: This can only be done by the logged in user.
operationId: deleteUser
parameters:
- name: username
in: path
description: The name that needs to be deleted
required: true
schema:
type: string
responses:
'400':
description: Invalid username supplied
'404':
description: User not found
/user/createWithArray:
post:
tags:
- user
summary: Creates list of users with given input array
description: ''
operationId: createUsersWithArrayInput
responses:
default:
description: successful operation
requestBody:
$ref: '#/components/requestBodies/UserArray'
/user/createWithList:
post:
tags:
- user
summary: Creates list of users with given input array
description: ''
operationId: createUsersWithListInput
responses:
default:
description: successful operation
requestBody:
$ref: '#/components/requestBodies/UserArray'
/user/login:
get:
tags:
- user
summary: Logs user into the system
description: ''
operationId: loginUser
parameters:
- name: username
in: query
description: The user name for login
required: true
schema:
type: string
- name: password
in: query
description: The password for login in clear text
required: true
schema:
type: string
responses:
'200':
description: successful operation
headers:
X-Rate-Limit:
description: calls per hour allowed by the user
schema:
type: integer
format: int32
X-Expires-After:
description: date in UTC when toekn expires
schema:
type: string
format: date-time
content:
application/json:
schema:
type: string
examples:
response:
value: OK
application/xml:
schema:
type: string
examples:
response:
value: <Message> OK </Message>
text/plain:
examples:
response:
value: OK
'400':
description: Invalid username/password supplied
/user/logout:
get:
tags:
- user
summary: Logs out current logged in user session
description: ''
operationId: logoutUser
responses:
default:
description: successful operation
components:
schemas:
ApiResponse:
type: object
properties:
code:
type: integer
format: int32
type:
type: string
message:
type: string
Cat:
description: A representation of a cat
allOf:
- $ref: '#/components/schemas/Pet'
- type: object
properties:
huntingSkill:
type: string
description: The measured skill for hunting
default: lazy
enum:
- clueless
- lazy
- adventurous
- aggressive
required:
- huntingSkill
Category:
type: object
properties:
id:
description: Category ID
allOf:
- $ref: '#/components/schemas/Id'
name:
description: Category name
type: string
minLength: 1
sub:
description: Test Sub Category
type: object
properties:
prop1:
type: string
description: Dumb Property
xml:
name: Category
Dog:
description: A representation of a dog
allOf:
- $ref: '#/components/schemas/Pet'
- type: object
properties:
packSize:
type: integer
format: int32
description: The size of the pack the dog is from
default: 1
minimum: 1
required:
- packSize
HoneyBee:
description: A representation of a honey bee
allOf:
- $ref: '#/components/schemas/Pet'
- type: object
properties:
honeyPerDay:
type: number
description: Average amount of honey produced per day in ounces
example: 3.14
required:
- honeyPerDay
Id:
type: integer
format: int64
readOnly: true
Order:
type: object
properties:
id:
description: Order ID
allOf:
- $ref: '#/components/schemas/Id'
petId:
description: Pet ID
allOf:
- $ref: '#/components/schemas/Id'
quantity:
type: integer
format: int32
minimum: 1
default: 1
shipDate:
description: Estimated ship date
type: string
format: date-time
status:
type: string
description: Order Status
enum:
- placed
- approved
- delivered
complete:
description: Indicates whenever order was completed or not
type: boolean
default: false
xml:
name: Order
Pet:
type: object
required:
- name
- photoUrls
discriminator:
propertyName: petType
mapping:
cat: '#/components/schemas/Cat'
dog: '#/components/schemas/Dog'
bee: '#/components/schemas/HoneyBee'
properties:
id:
description: Pet ID
allOf:
- $ref: '#/components/schemas/Id'
category:
description: Categories this pet belongs to
allOf:
- $ref: '#/components/schemas/Category'
name:
description: The name given to a pet
type: string
example: Guru
photoUrls:
description: The list of URL to a cute photos featuring pet
type: array
xml:
name: photoUrl
wrapped: true
items:
type: string
format: url
friend:
allOf:
- $ref: '#/components/schemas/Pet'
tags:
description: Tags attached to the pet
type: array
xml:
name: tag
wrapped: true
items:
$ref: '#/components/schemas/Tag'
status:
type: string
description: Pet status in the store
enum:
- available
- pending
- sold
petType:
description: Type of a pet
type: string
xml:
name: Pet
Tag:
type: object
properties:
id:
description: Tag ID
allOf:
- $ref: '#/components/schemas/Id'
name:
description: Tag name
type: string
minLength: 1
xml:
name: Tag
User:
type: object
properties:
id:
$ref: '#/components/schemas/Id'
pet:
oneOf:
- $ref: '#/components/schemas/Pet'
- $ref: '#/components/schemas/Tag'
username:
description: User supplied username
type: string
minLength: 4
example: John78
firstName:
description: User first name
type: string
minLength: 1
example: John
lastName:
description: User last name
type: string
minLength: 1
example: Smith
email:
description: User email address
type: string
format: email
example: john.smith@example.com
password:
type: string
description: >-
User password, MUST contain a mix of upper and lower case letters,
as well as digits
format: password
minLength: 8
pattern: '(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])'
example: drowssaP123
phone:
description: User phone number in international format
type: string
pattern: '^\+(?:[0-9]-?){6,14}[0-9]$'
example: +1-202-555-0192
nullable: true
userStatus:
description: User status
type: integer
format: int32
xml:
name: User
requestBodies:
Pet:
content:
application/json:
schema:
allOf:
- description: My Pet
title: Pettie
- $ref: '#/components/schemas/Pet'
application/xml:
schema:
type: 'object'
properties:
name:
type: string
description: hooray
description: Pet object that needs to be added to the store
required: true
UserArray:
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/User'
description: List of user object
required: true
securitySchemes:
petstore_auth:
description: |
Get access to data while protecting your account credentials.
OAuth2 is also a safer and more secure way to give you access.
type: oauth2
flows:
implicit:
authorizationUrl: 'http://petstore.swagger.io/api/oauth/dialog'
scopes:
'write:pets': modify pets in your account
'read:pets': read your pets
api_key:
description: >
For this sample, you can use the api key `special-key` to test the
authorization filters.
type: apiKey
name: api_key
in: header

View File

@ -0,0 +1,50 @@
import * as React from 'react';
import { render } from 'react-dom';
import { AppContainer } from 'react-hot-loader';
// import DevTools from 'mobx-react-devtools';
import { Redoc, RedocProps } from '../../src/components/Redoc/Redoc';
import { AppStore } from '../../src/services/AppStore';
import { RedocRawOptions } from '../../src/services/RedocNormalizedOptions';
import { loadAndBundleSpec } from '../../src/utils/loadAndBundleSpec';
const renderRoot = (props: RedocProps) =>
render(
<AppContainer>
<Redoc {...props} />
</AppContainer>,
document.getElementById('example'),
);
const big = window.location.search.indexOf('big') > -1;
const swagger = window.location.search.indexOf('swagger') > -1; // compatibility mode ?
const specUrl = swagger ? 'swagger.yaml' : big ? 'big-openapi.json' : 'openapi.yaml';
let store;
const options: RedocRawOptions = { nativeScrollbars: false };
async function init() {
const spec = await loadAndBundleSpec(specUrl);
store = new AppStore(spec, specUrl, options);
renderRoot({ store });
}
init();
if (module.hot) {
const reload = (reloadStore = false) => async () => {
if (reloadStore) {
// create a new Store
store.dispose();
const state = await store.toJS();
store = AppStore.fromJS(state);
}
renderRoot({ store });
};
module.hot.accept(['../../src/components/Redoc/Redoc'], reload());
module.hot.accept(['../../src/services/AppStore'], reload(true));
}

View File

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>ReDoc</title>
<style>
body {
margin: 0;
padding: 0;
}
redoc {
display: block;
}
</style>
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
</head>
<body>
<redoc id="example"></redoc>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 198 KiB

67
demo/ssr/index.ts Normal file
View File

@ -0,0 +1,67 @@
import { renderToString } from 'react-dom/server';
import * as React from 'react';
import { ServerStyleSheet } from 'styled-components';
// @ts-ignore
import { Redoc, createStore } from '../../';
import { readFileSync } from 'fs';
import { resolve } from 'path';
const yaml = require('yaml-js');
const http = require('http');
const url = require('url');
const fs = require('fs');
const PORT = 9999;
const server = http.createServer(async (request, response) => {
console.time('request ' + request.url);
if (request.url === '/redoc.standalone.js') {
fs.createReadStream('bundles/redoc.standalone.js', 'utf8').pipe(response);
} else if (request.url === '/') {
const spec = yaml.load(readFileSync(resolve(__dirname, '../openapi.yaml')));
let store = await createStore(spec, 'path/to/spec.yaml');
const sheet = new ServerStyleSheet();
const html = renderToString(sheet.collectStyles(React.createElement(Redoc, { store })));
const css = sheet.getStyleTags();
const res = `<html>
<head>
<meta charset="utf8" />
<title>ReDoc</title>
<!-- needed for adaptive design -->
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {
padding: 0;
margin: 0;
}
</style>
<script src="redoc.standalone.js"></script>
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
${css}
</head>
<body>
<script>
document.addEventListener('DOMContentLoaded', function() {
const state = ${JSON.stringify(await store.toJS())};
Redoc.hydrate(state, document.getElementById('redoc'));
});
</script>
<div id="redoc">${html}</div>
</body>
</html>`;
response.writeHead(200, { 'Content-Length': res.length });
response.write(res);
response.end();
} else {
response.writeHead(404);
response.write('Not found');
response.end();
}
console.timeEnd('request ' + request.url);
});
server.listen(PORT, () => console.log(`Server started: http://127.0.0.1:${PORT}`));

118
demo/webpack.config.ts Normal file
View File

@ -0,0 +1,118 @@
import * as webpack from 'webpack';
import * as HtmlWebpackPlugin from 'html-webpack-plugin';
import * as ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin';
import { resolve } from 'path';
const VERSION = JSON.stringify(require('../package.json').version);
const REVISION = JSON.stringify(
require('child_process')
.execSync('git rev-parse --short HEAD')
.toString()
.trim(),
);
function root(filename) {
return resolve(__dirname + '/' + filename);
}
const tsLoader = env => ({
loader: 'ts-loader',
options: {
compilerOptions: {
module: env.bench ? 'esnext' : 'es2015',
},
},
});
const babelHotLoader = {
loader: 'babel-loader',
options: {
plugins: [
'@babel/plugin-syntax-typescript',
'@babel/plugin-syntax-decorators',
'@babel/plugin-syntax-jsx',
'react-hot-loader/babel',
],
},
};
export default (env: { playground?: boolean; bench?: boolean } = {}, { mode }) => ({
entry: [
root('../src/polyfills.ts'),
root(
env.playground
? 'playground/hmr-playground.tsx'
: env.bench ? '../benchmark/index.tsx' : 'index.tsx',
),
],
output: {
filename: 'redoc-demo.bundle.js',
path: root('dist'),
globalObject: 'this',
},
devServer: {
contentBase: __dirname,
watchContentBase: true,
port: 9090,
disableHostCheck: true,
stats: 'minimal',
},
resolve: {
extensions: ['.ts', '.tsx', '.js', '.json'],
},
node: {
fs: 'empty',
},
performance: false,
module: {
rules: [
{ enforce: 'pre', test: /\.js$/, loader: 'source-map-loader' },
{ test: [/\.eot$/, /\.gif$/, /\.woff$/, /\.svg$/, /\.ttf$/], use: 'null-loader' },
{
test: /\.tsx?$/,
use: mode === 'production' ? [tsLoader(env)] : [tsLoader(env), babelHotLoader],
exclude: ['node_modules'],
},
{
test: /\.css$/,
use: {
loader: 'css-loader',
options: {
sourceMap: true,
minimize: true,
},
},
},
{
test: /node_modules\/(swagger2openapi|reftools)\/.*\.js$/,
use: {
loader: 'ts-loader',
options: {
transpileOnly: true,
instance: 'ts2js-transpiler-only',
compilerOptions: {
allowJs: true,
},
},
},
},
],
},
plugins: [
new webpack.DefinePlugin({
__REDOC_VERSION__: VERSION,
__REDOC_REVISION__: REVISION,
}),
new webpack.NamedModulesPlugin(),
new webpack.optimize.ModuleConcatenationPlugin(),
new HtmlWebpackPlugin({
template: env.playground ? 'demo/playground/index.html' : 'demo/index.html',
}),
new ForkTsCheckerWebpackPlugin(),
],
});

View File

@ -0,0 +1,47 @@
describe('Menu', () => {
beforeEach(() => {
cy.visit('e2e/standalone.html');
});
it('should have valid items count', function() {
cy
.get('.menu-content')
.find('li')
.should('have.length', 6 + (2 + 8 + 4) + (1 + 8));
});
it('should sync active menu items while scroll', function() {
cy
.contains('h1', 'Introduction')
.scrollIntoView()
.get('[role=menuitem].active:not(.-depth0)')
.should('have.text', 'Introduction');
cy
.contains('h2', 'Add a new pet to the store')
.scrollIntoView()
.get('[role=menuitem].active:not(.-depth0)')
.should('have.length', 2)
.last()
.should('have.text', 'Add a new pet to the store')
.should('be.visible');
});
it('should update URL hash when clicking on menu items', function() {
cy.contains('[role=menuitem].-depth1', 'pet').click({ force: true });
cy.location('hash').should('equal', '#tag/pet');
cy.contains('[role=menuitem]', 'Find pet by ID').click({ force: true });
cy.location('hash').should('equal', '#operation/getPetById');
});
it('should deactivate tag when other is activated', function() {
const petItem = () => cy.contains('[role=menuitem].-depth1', 'pet');
petItem()
.click({ force: true })
.should('have.class', 'active');
cy.contains('[role=menuitem].-depth1', 'store').click({ force: true });
petItem().should('not.have.class', 'active');
});
});

View File

@ -0,0 +1,55 @@
describe('Search', () => {
const getSearchInput = () => cy.get('[role="search"] input');
const getSearchResults = () => cy.get('[data-role="search:results"]');
const getResult = i => cy.get('[role=search] [role=menuitem]').eq(i);
beforeEach(() => {
cy.visit('e2e/standalone.html');
});
it('should correctly show and hide search results box', function() {
getSearchResults().should('not.exist');
// should not open for less than 3 symbols
getSearchInput().type('in', { force: true });
getSearchResults().should('not.exist');
getSearchInput().type('t', { force: true });
cy
.get('[role=search] [role=menuitem]')
.should('have.length', 3)
.first()
.should('contain', 'Introduction');
getSearchInput().type('{esc}', { force: true });
getSearchResults().should('not.exist');
});
it('should support arrow navigation', function() {
getSearchInput().type('int', { force: true });
getSearchInput().type('{downarrow}', { force: true });
getResult(0).should('have.class', 'active');
getSearchInput().type('{downarrow}', { force: true });
getResult(1).should('have.class', 'active');
getResult(0).should('not.have.class', 'active');
getSearchInput().type('{uparrow}', { force: true });
getResult(1).should('not.have.class', 'active');
getResult(0).should('have.class', 'active');
getSearchInput().type('{uparrow}', { force: true });
getResult(0).should('have.class', 'active');
getSearchInput().type('{enter}', { force: true });
cy.contains('[role=navigation] [role=menuitem]', 'Introduction').should('have.class', 'active');
});
it('should mark search results', function() {
cy.get('[data-markjs]').should('not.exist');
getSearchInput().type('int', { force: true });
cy.get('[data-markjs]').should('exist');
});
});

View File

@ -0,0 +1,20 @@
describe('Standalone bundle test', function() {
function baseCheck(name: string, url: string) {
describe(name, () => {
before(() => {
cy.visit(url);
});
it('Render and check no errors', function() {
cy.get('.api-info').should('exist');
});
it('Render and click all the menu items', function() {
cy.get('.menu-content li').click({ multiple: true, force: true });
});
});
}
baseCheck('OAS3 mode', 'e2e/standalone.html');
baseCheck('OAS2 compatibility mode', 'e2e/standalone-compatibility.html');
});

View File

@ -0,0 +1,30 @@
const wp = require('@cypress/webpack-preprocessor');
const webpackOptions = {
resolve: {
extensions: ['.ts', '.js'],
},
performance: false,
module: {
rules: [
{
test: /\.ts$/,
exclude: [/node_modules/],
use: [
{
loader: 'ts-loader',
options: {
configFile: 'e2e/tsconfig.json',
},
},
],
},
],
},
};
const options = {
webpackOptions,
};
module.exports = wp(options);

5
e2e/plugins/index.js Normal file
View File

@ -0,0 +1,5 @@
const cypressTypeScriptPreprocessor = require('./cy-ts-preprocessor');
module.exports = on => {
on('file:preprocessor', cypressTypeScriptPreprocessor);
};

View File

@ -0,0 +1,8 @@
<html>
<body>
<redoc spec-url="../demo/swagger.yaml"></redoc>
<script src="../bundles/redoc.standalone.js"></script>
</body>
</html>

8
e2e/standalone.html Normal file
View File

@ -0,0 +1,8 @@
<html>
<body>
<redoc spec-url="../demo/openapi.yaml" native-scrollbars></redoc>
<script src="../bundles/redoc.standalone.js"></script>
</body>
</html>

27
e2e/tsconfig.json Normal file
View File

@ -0,0 +1,27 @@
{
"compilerOptions": {
"experimentalDecorators": true,
"module": "commonjs",
"moduleResolution": "node",
"target": "es2015",
"noImplicitAny": false,
"noUnusedLocals": true,
"noUnusedParameters": true,
"strictNullChecks": true,
"sourceMap": true,
"pretty": true,
"lib": [
"es2015",
"es2016",
"es2017",
"dom"
],
"jsx": "react",
"types": ["cypress"]
},
"compileOnSave": false,
"include": [
"integration/*.ts",
"../node_modules/cypress"
]
}

View File

@ -1,13 +0,0 @@
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RedocModule } from './redoc.module';
import { Redoc } from './components/index';
@NgModule({
imports: [ BrowserModule, RedocModule ],
bootstrap: [ Redoc ],
exports: [ Redoc ]
})
export class AppModule {
}

View File

@ -1,7 +0,0 @@
import { NgModuleRef } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app.module';
export function bootstrapRedoc(): Promise<NgModuleRef<AppModule>> {
return platformBrowserDynamic().bootstrapModule(AppModule);
}

View File

@ -1,9 +0,0 @@
import { NgModuleRef } from '@angular/core';
import { platformBrowser } from '@angular/platform-browser';
import { AppModule } from './app.module';
// @ts-ignore
import { AppModuleNgFactory } from '../compiled/lib/app.module.ngfactory';
export function bootstrapRedoc():Promise<NgModuleRef<AppModule>> {
return platformBrowser().bootstrapModuleFactory(AppModuleNgFactory);
}

View File

@ -1,24 +0,0 @@
<div class="api-info-wrapper">
<h1>{{info.title}} <span class="api-info-version">({{info.version}})</span></h1>
<p class="download-openapi" *ngIf="specUrl && !hideDownloadButton">
Download OpenAPI specification:
<a class="openapi-button" [attr.download]="downloadFilename" [attr.href]="specUrl"> Download </a>
</p>
<p>
<!-- TODO: create separate components for contact and license ? -->
<span *ngIf="info?.contact?.url || info?.contact?.email"> Contact:
<a *ngIf="info.contact.url" href="{{info.contact.url}}">
{{info.contact.name || info.contact.url}}</a>
<a *ngIf="info.contact.email" href="mailto:{{info.contact.email}}">
{{info.contact.email}}</a>
</span>
<span *ngIf="info.license"> License:
<a *ngIf="info.license.url" href="{{info.license.url}}"> {{info.license.name}} </a>
<span *ngIf="!info.license.url"> {{info.license.name}} </span>
</span>
<redoc-externalDocs [docs]="componentSchema.externalDocs"></redoc-externalDocs>
</p>
<span class="redoc-markdown-block">
<dynamic-ng2-viewer [html]="info['x-redoc-html-description']"></dynamic-ng2-viewer>
</span>
</div>

View File

@ -1,33 +0,0 @@
@import '../../shared/styles/variables';
:host > .api-info-wrapper {
box-sizing: border-box;
padding: $section-spacing;
width: 60%;
@media (max-width: $right-panel-squash-breakpoint) {
width: 100%;
}
@media (max-width: $side-menu-mobile-breakpoint) {
padding-top: $section-spacing + 20px;
}
}
.openapi-button {
border: 1px solid $primary-color;
color: $primary-color;
font-weight: normal;
margin-left: 0.5em;
padding: 3px 8px 4px;
display: inline-block;
}
:host /deep/ [section] {
padding-top: 2 * $section-spacing;
}
:host /deep/ h2[section] {
padding-top: $section-spacing;
}

View File

@ -1,66 +0,0 @@
'use strict';
import { getChildDebugElement } from '../../../tests/helpers';
import { Component } from '@angular/core';
import { OptionsService } from '../../services/index';
import {
inject,
async,
TestBed
} from '@angular/core/testing';
import { SpecManager } from '../../utils/spec-manager';
describe('Redoc components', () => {
describe('ApiInfo Component', () => {
let component;
let fixture;
let opts;
let specMgr;
beforeEach(() => {
TestBed.configureTestingModule({ declarations: [ TestAppComponent ] });
});
beforeEach(async(inject([SpecManager, OptionsService], (_specMgr, _opts) => {
opts = _opts;
opts.options = {
scrollYOffset: () => 0,
$scrollParent: window
};
specMgr = _specMgr;
})));
beforeEach(done => {
specMgr.load('/tests/schemas/api-info-test.json').then(done, done.fail);
});
beforeEach(async(() => {
fixture = TestBed.createComponent(TestAppComponent);
component = getChildDebugElement(fixture.debugElement, 'api-info').componentInstance;
fixture.detectChanges();
}));
it('should init component data', () => {
expect(component).not.toBeNull();
expect(component.info).not.toBeNull();
component.info.title.should.be.equal('Swagger Petstore');
});
it('should render api name and version', () => {
let nativeElement = getChildDebugElement(fixture.debugElement, 'api-info').nativeElement;
let headerElement = nativeElement.querySelector('h1');
expect(headerElement.innerText).toContain('Swagger Petstore (v1.0.0)');
});
});
});
/** Test component that contains an ApiInfo. */
@Component({
selector: 'test-app',
template:
`<api-info></api-info>`
})
class TestAppComponent {
}

View File

@ -1,47 +0,0 @@
'use strict';
import { Component, ChangeDetectionStrategy, OnInit, ElementRef } from '@angular/core';
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
import { SpecManager, BaseComponent } from '../base';
import { OptionsService, Marker } from '../../services/index';
@Component({
selector: 'api-info',
styleUrls: ['./api-info.css'],
templateUrl: './api-info.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ApiInfo extends BaseComponent implements OnInit {
info: any = {};
specUrl: String | SafeResourceUrl;
downloadFilename = '';
hideDownloadButton = this.optionsService.options.hideDownloadButton;
constructor(specMgr: SpecManager,
private optionsService: OptionsService,
elRef: ElementRef,
marker: Marker,
private sanitizer: DomSanitizer
) {
super(specMgr);
marker.addElement(elRef.nativeElement);
}
init() {
this.info = this.componentSchema.info;
this.specUrl = this.specMgr.specUrl;
if (!this.specUrl && window.Blob && window.URL) {
const blob = new Blob([JSON.stringify(this.specMgr.rawSpec, null, 2)], {type : 'application/json'});
this.specUrl = this.sanitizer.bypassSecurityTrustResourceUrl(window.URL.createObjectURL(blob));
this.downloadFilename = 'swagger.json';
}
if (!isNaN(parseInt(this.info.version.toString().substring(0, 1)))) {
this.info.version = 'v' + this.info.version;
}
}
ngOnInit() {
this.preinit();
}
}

View File

@ -1,4 +0,0 @@
<a *ngIf="logo.url" href="{{logo.url}}">
<img *ngIf="logo.imgUrl" [attr.src]="logo.imgUrl" [ngStyle]="{'background-color': logo.bgColor}">
</a>
<img *ngIf="logo.imgUrl && !logo.url" [attr.src]="logo.imgUrl" [ngStyle]="{'background-color': logo.bgColor}">

View File

@ -1,19 +0,0 @@
@import '../../shared/styles/variables';
:host {
display: block;
text-align: center;
@media (max-width: $side-menu-mobile-breakpoint) {
display: none;
}
}
img {
max-height: 150px;
width: auto;
display: inline-block;
max-width: 100%;
//padding: 0 5px;
box-sizing: border-box;
}

View File

@ -1,73 +0,0 @@
'use strict';
import { getChildDebugElement } from '../../../tests/helpers';
import { Component } from '@angular/core';
import {
inject,
async,
TestBed
} from '@angular/core/testing';
import { SpecManager } from '../../utils/spec-manager';
describe('Redoc components', () => {
describe('ApiLogo Component', () => {
let builder;
let component;
let fixture;
let specMgr;
let schemaUrl = '/tests/schemas/api-info-test.json';
beforeEach(() => {
TestBed.configureTestingModule({ declarations: [ TestAppComponent ] });
});
beforeEach(async(inject([SpecManager], ( _specMgr) => {
specMgr = _specMgr;
})));
beforeEach(done => {
specMgr.load(schemaUrl).then(done, done.fail);
});
beforeEach(() => {
fixture = TestBed.createComponent(TestAppComponent);
component = getChildDebugElement(fixture.debugElement, 'api-logo').componentInstance;
fixture.detectChanges();
});
it('should init component data', () => {
if (specMgr.a) return;
expect(component).not.toBeNull();
expect(component.logo).not.toBeNull();
});
it('should not display image when no x-logo', () => {
component.logo.should.be.empty();
let nativeElement = getChildDebugElement(fixture.debugElement, 'api-logo').nativeElement;
let imgElement = nativeElement.querySelector('img');
expect(imgElement).toBeNull();
// update schemaUrl to load other schema in the next test
schemaUrl = '/tests/schemas/extended-petstore.yml';
});
it('should load values from spec and use transparent bgColor by default', () => {
component.logo.imgUrl.should.endWith('petstore-logo.png');
component.logo.bgColor.should.be.equal('transparent');
});
});
});
/** Test component that contains an ApiInfo. */
@Component({
selector: 'test-app',
template:
`<api-logo></api-logo>`
})
class TestAppComponent {
}

View File

@ -1,30 +0,0 @@
'use strict';
import { Component, ChangeDetectionStrategy, OnInit } from '@angular/core';
import { BaseComponent, SpecManager } from '../base';
@Component({
selector: 'api-logo',
styleUrls: ['./api-logo.css'],
templateUrl: './api-logo.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ApiLogo extends BaseComponent implements OnInit {
logo:any = {};
constructor(specMgr:SpecManager) {
super(specMgr);
}
init() {
const info = this.componentSchema.info;
const logoInfo = info['x-logo'];
if (!logoInfo) return;
this.logo.imgUrl = logoInfo.url;
this.logo.bgColor = logoInfo.backgroundColor || 'transparent';
this.logo.url = info.contact && info.contact.url || null;
}
ngOnInit() {
this.preinit();
}
}

View File

@ -1,17 +0,0 @@
<div class="operation-endpoint" (click)="handleClick()">
<h5 class="http-verb" [ngClass]="verb">{{verb}}</h5>
<span><!--
--><span class="operation-api-url-path">{{path}}</span><!--
--></span>
<svg class="expand-icon" xmlns="http://www.w3.org/2000/svg" version="1.1" x="0" y="0" viewBox="0 0 24 24" xml:space="preserve">
<polygon fill="white" points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "/>
</svg>
</div>
<div class="servers-overlay">
<div *ngFor="let server of servers" class="server-item">
<div class="description" [innerHtml]="server.description | marked"></div>
<div select-on-click class="url">
<span class="operation-api-url"> {{server.url}}</span>{{path}}
</div>
</div>
</div>

View File

@ -1,153 +0,0 @@
@import '../../shared/styles/variables';
:host {
display: block;
position: relative;
cursor: pointer;
}
.operation-endpoint {
padding: 10px 30px 10px 20px;
border-radius: $border-radius*2;
background-color: darken($black, 2%);
display: block;
font-weight: $light;
white-space: nowrap;
overflow-x: hidden;
text-overflow: ellipsis;
border: 1px solid transparent;
}
.operation-endpoint > .operation-params-subheader {
padding-top: 1px;
padding-bottom: 0;
margin: 0;
font-size: 12/14em;
color: $black;
vertical-align: middle;
display: inline-block;
border-radius: $border-radius;
}
.operation-api-url {
color: rgba($black, 0.8);
&-path {
font-family: $headers-font, $headers-font-family;
position: relative;
top: 1px;
color: #ffffff;
margin-left: 10px;
}
}
.http-verb {
color: $black;
background: #ffffff;
padding: 3px 10px;
text-transform: uppercase;
display: inline-block;
margin: 0;
}
.servers-overlay {
position: absolute;
width: 100%;
z-index: 100;
background: $side-bar-bg-color;
color: $black;
box-sizing: border-box;
box-shadow: 4px 4px 6px rgba(0, 0, 0, 0.33);
overflow: hidden;
border-bottom-left-radius: $border-radius*2;
border-bottom-right-radius: $border-radius*2;
}
.server-item {
padding: 10px;
//margin-bottom: 10px;
& > .url {
padding: 5px;
border: 1px solid $border-color;
background: $background-color;
word-break: break-all;
color: $primary-color;
}
&:last-child {
margin-bottom: 0;
}
}
.expand-icon {
height: 20px;
width: 20px;
display: inline-block;
float: right;
background: darken($black, 2%);
transform: rotateZ(0);
transition: all 0.2s ease;
top: 15px;
right: 5px;
position: absolute;
}
.servers-overlay {
transform: translateY(-50%) scaleY(0);
transition: all 0.25s ease;
}
:host.expanded {
> .operation-endpoint {
border-color: $side-bar-bg-color;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
.expand-icon {
transform: rotateZ(180deg);
}
.servers-overlay {
transform: translateY(0%) scaleY(1);
}
}
.http-verb {
color: white;
&.get {
background-color: $get-color;
}
&.post {
background-color: $post-color;
}
&.put {
background-color: $put-color;
}
&.options {
background-color: $options-color;
}
&.patch {
background-color: $patch-color;
}
&.delete {
background-color: $delete-color;
}
&.basic {
background-color: $basic-color;
}
&.link {
background-color: $link-color;
}
&.head {
background-color: $head-color;
}
}

View File

@ -1,82 +0,0 @@
'use strict';
import { Component } from '@angular/core';
import {
inject,
async,
TestBed
} from '@angular/core/testing';
import { getChildDebugElement } from '../../../tests/helpers';
import { EndpointLink } from './endpoint-link';
import { SpecManager } from '../../utils/spec-manager';
import { OptionsService } from '../../services/';
describe('Redoc components', () => {
beforeEach(() => {
TestBed.configureTestingModule({ declarations: [ TestAppComponent ] });
});
describe('EndpointLink Component', () => {
let builder;
let component: EndpointLink;
let specMgr: SpecManager;
let opts: OptionsService;
beforeEach(async(inject([SpecManager, OptionsService], (_specMgr, _opts) => {
specMgr = _specMgr;
opts = _opts;
})));
beforeEach(() => {
specMgr.apiUrl = 'http://test.com/v1';
specMgr._schema = {
info: {},
host: 'petstore.swagger.io',
baseName: '/v2',
schemes: ['https', 'http'],
'x-servers': [
{
url: '//test.com/v2'
},
{
url: 'ws://test.com/v3',
description: 'test'
}
]
};
specMgr.init();
component = new EndpointLink(specMgr, opts);
});
it('should replace // with appropriate protocol', () => {
component.ngOnInit();
component.servers[0].url.should.be.equal('https://test.com/v2');
});
it('should preserve other protocols', () => {
component.ngOnInit();
component.servers[1].url.should.be.equal('ws://test.com/v3');
});
it('should fallback to host + basePath + schemas if no x-servers', () => {
specMgr._schema['x-servers'] = null;
specMgr.init();
component.ngOnInit();
component.servers.should.be.lengthOf(1);
component.servers[0].url.should.be.equal('https://petstore.swagger.io');
});
});
});
/** Test component that contains an Operation. */
@Component({
selector: 'test-app',
template:
`<operation pointer='#/paths/~1user~1{username}/put'></operation>`
})
class TestAppComponent {
}

View File

@ -1,63 +0,0 @@
'use strict';
import { Component, ChangeDetectionStrategy, Input, OnInit, HostListener, HostBinding} from '@angular/core';
import { BaseComponent, SpecManager } from '../base';
import { OptionsService } from '../../services/';
import { stripTrailingSlash } from '../../utils/';
export interface ServerInfo {
description: string;
url: string;
}
@Component({
selector: 'endpoint-link',
styleUrls: ['./endpoint-link.css'],
templateUrl: './endpoint-link.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class EndpointLink implements OnInit {
@Input() path:string;
@Input() verb:string;
apiUrl: string;
servers: ServerInfo[];
@HostBinding('class.expanded') expanded: boolean = false;
// @HostListener('click')
handleClick() {
this.expanded = !this.expanded;
}
constructor(public specMgr:SpecManager, public optionsService: OptionsService) {
this.expanded = false;
}
init() {
let servers:ServerInfo[] = this.specMgr.schema['x-servers'];
if (servers) {
this.servers = servers.map(({url, description}) => ({
description,
url: stripTrailingSlash(url.startsWith('//') ? `${this.specMgr.apiProtocol}:${url}` : url)
}));
} else {
this.servers = [
{
description: 'Server URL',
url: this.getBaseUrl()
}
];
}
}
getBaseUrl():string {
if (this.optionsService.options.hideHostname) {
return '';
} else {
return this.specMgr.apiUrl;
}
}
ngOnInit() {
this.init();
}
}

View File

@ -1,18 +0,0 @@
'use strict';
import { Component, Input, ChangeDetectionStrategy, OnInit } from '@angular/core';
import { BaseComponent, SpecManager } from '../base';
@Component({
selector: 'redoc-externalDocs',
template: `<a *ngIf="docs" [href]="docs.url" [innerHtml]="docs.description | marked"></a>`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ExternalDocs implements OnInit {
@Input() docs;
ngOnInit() {
if (this.docs && !this.docs.description) {
this.docs.description = 'External Docs';
}
}
}

View File

@ -1,260 +0,0 @@
@import '../../shared/styles/variables';
$lines-width: 1px;
$bullet-size: 1px;
$cell-spacing: 25px;
$cell-padding: 10px;
$bullet-margin: 10px;
$line-border: $lines-width solid $tree-lines-color;
$line-border-erase: ($lines-width + 1px) solid $background-color;
$border-color: lighten($secondary-color, 50%);
$nullable-color: #3195a6;
$hint-border: 1px dotted rgba(38, 50, 56, 0.4);
$param-name-height: 20px;
$sub-schema-offset: ($bullet-size / 2) + $bullet-margin;
.param-name-wrap {
display: inline-block;
padding-right: $cell-spacing;
font-family: $headers-font, $headers-font-family;
}
.param-info {
border-bottom: 1px solid $border-color;
padding: $cell-padding 0;
width: 75%;
box-sizing: border-box;
> div {
line-height: 1;
}
}
.param-range {
position: relative;
top: 1px;
margin-right: 6px;
margin-left: 6px;
border-radius: $border-radius;
background-color: rgba($primary-color, 0.1);
padding: 0 4px;
color: rgba($primary-color, 0.7);
}
.param-description {
//font-size: 14px;
}
.param-required {
vertical-align: middle;
line-height: $param-name-height;
color: $red;
font-size: 12px;
font-weight: bold;
}
.param-nullable {
vertical-align: middle;
line-height: $param-name-height;
color: $nullable-color;
font-size: 12px;
font-weight: bold;
}
.param-type,
.param-array-format {
vertical-align: middle;
line-height: $param-name-height;
color: rgba($black, 0.4);
font-size: 0.929em;
}
.param-type {
font-weight: normal;
word-break: break-all;
&.array::before,
&.tuple::before {
color: $black;
font-weight: $base-font-weight;
.param-collection-format-multi + & {
content: none;
}
}
&.array::before {
content: $array-text;
}
&.tuple::before {
content: $tuple-text;
}
&.with-hint {
display: inline-block;
margin-bottom: 0.4em;
border-bottom: $hint-border;
padding: 0;
cursor: help;
}
&-trivial {
display: inline-block;
}
&-file {
font-weight: bold;
text-transform: capitalize;
}
}
// tree
// Bullet
.param-name {
border-left: $line-border;
box-sizing: border-box;
position: relative;
padding: $cell-padding 0;
vertical-align: top;
line-height: $param-name-height;
white-space: nowrap;
font-size: 0.929em;
font-weight: $regular;
> span::before {
content: '';
display: inline-block;
width: $bullet-size;
height: $bullet-size + 6;
background-color: $primary-color;
margin: 0 $bullet-margin;
vertical-align: middle;
}
> span::after {
content: '';
position: absolute;
border-top: $line-border;
width: $bullet-margin;
left: 0;
top: ($param-name-height / 2) + $cell-padding + 1;
}
}
.param:first-of-type {
> .param-name::before {
content: '';
display: block;
position: absolute;
left: -$lines-width;
top: 0;
border-left: $line-border-erase;
height: ($param-name-height / 2) + $cell-padding + 1;
}
}
.param:last-of-type,
.param.last {
> .param-name {
position: relative;
&::after {
content: '';
display: block;
position: absolute;
left: -$lines-width - 1px;
border-left: $line-border-erase;
top: ($param-name-height / 2) + $cell-padding + $lines-width + 1;
background-color: $background-color;
bottom: 0;
}
}
}
.param-wrap:last-of-type > .param-schema {
border-left-color: transparent;
}
.param-schema {
.param-wrap:first-of-type {
.param-name::before {
display: none;
}
}
}
.param-schema.last {
> td {
border-left: 0;
}
}
.param-enum {
color: $text-color;
font-size: 0.95em;
&::before {
content: 'Valid values: ';
}
}
.param-enum {
color: $text-color;
font-size: 0.95em;
&::before {
content: 'Valid values: ';
}
.param-type.array ~ &::before {
content: 'Valid items values: ';
}
}
.param-pattern {
color: $nullable-color;
white-space: nowrap;
&::before,
&::after {
content: '/';
margin: 0 3px;
font-size: 1.2em;
font-weight: bold;
}
}
.param-default {
font-size: 0.95em;
&::before {
content: 'Default: ';
}
}
.param-example {
font-size: 0.95em;
&::before {
content: 'Example: ';
}
}
.param-enum-value,
.param-default-value,
.param-example-value {
font-family: Courier, monospace;
background-color: rgba($secondary-color, 0.02);
border: 1px solid rgba($secondary-color, 0.1);
margin: 2px 3px;
padding: 0.1em 0.2em 0.2em;
border-radius: $border-radius;
color: $text-color;
display: inline-block;
min-width: 20px;
text-align: center;
}

View File

@ -1,55 +0,0 @@
'use strict';
import { getChildDebugElement } from '../../../tests/helpers';
import { Component } from '@angular/core';
import {
inject,
TestBed
} from '@angular/core/testing';
import { JsonSchemaLazy } from './json-schema-lazy';
describe('Redoc components', () => {
beforeEach(() => {
TestBed.configureTestingModule({ declarations: [ TestAppComponent ] });
});
describe('JsonSchemaLazy Component', () => {
let builder;
let component;
let fixture;
beforeEach(() => {
fixture = TestBed.createComponent(TestAppComponent);
let debugEl = getChildDebugElement(fixture.debugElement, 'json-schema-lazy');
component = <JsonSchemaLazy>debugEl.componentInstance;
spyOn(component, '_loadAfterSelf').and.stub();
});
afterEach(() => {
component._loadAfterSelf.and.callThrough();
});
it('should init component', () => {
expect(component).not.toBeNull();
});
it('should run loadNextToLocation on load', () => {
component.pointer = '#/def';
fixture.detectChanges();
component.load();
expect(component._loadAfterSelf).toHaveBeenCalled();
});
});
});
/** Test component that contains a lazy schema. */
@Component({
selector: 'test-app',
template:
`<json-schema-lazy></json-schema-lazy>`
})
class TestAppComponent {
}

View File

@ -1,100 +0,0 @@
'use strict';
import { Component, ElementRef, ViewContainerRef, OnDestroy, OnInit, Input,
AfterViewInit, ComponentFactoryResolver, Renderer } from '@angular/core';
import { JsonSchema } from './json-schema';
import { OptionsService } from '../../services/options.service';
import { SpecManager } from '../../utils/spec-manager';
var cache = {};
@Component({
selector: 'json-schema-lazy',
entryComponents: [ JsonSchema ],
template: '',
styles: [':host { display:none }']
})
export class JsonSchemaLazy implements OnDestroy, OnInit, AfterViewInit {
@Input() pointer: string;
@Input() absolutePointer: string;
@Input() auto: boolean;
@Input() isRequestSchema: boolean;
@Input() final: boolean = false;
@Input() nestOdd: boolean;
@Input() childFor: string;
@Input() isArray: boolean;
disableLazy: boolean = false;
loaded: boolean = false;
constructor(private specMgr:SpecManager, private location:ViewContainerRef, private elementRef:ElementRef,
private resolver:ComponentFactoryResolver, private optionsService:OptionsService, private _renderer: Renderer) {
this.disableLazy = this.optionsService.options.disableLazySchemas;
}
normalizePointer() {
let schema = this.specMgr.byPointer(this.pointer);
return schema && schema.$ref || this.pointer;
}
private _loadAfterSelf() {
var componentFactory = this.resolver.resolveComponentFactory(JsonSchema);
let contextInjector = this.location.parentInjector;
let compRef = this.location.createComponent(componentFactory, null, contextInjector, null);
this.projectComponentInputs(compRef.instance);
this._renderer.setElementAttribute(compRef.location.nativeElement, 'class', this.location.element.nativeElement.className);
compRef.changeDetectorRef.detectChanges();
this.loaded = true;
return compRef;
}
load() {
if (this.disableLazy) return;
if (this.loaded) return;
if (this.pointer) {
this._loadAfterSelf();
}
}
// cache JsonSchema view
loadCached() {
this.pointer = this.normalizePointer();
if (cache[this.pointer]) {
let compRef = cache[this.pointer];
let $element = compRef.location.nativeElement;
// skip caching view with descendant schemas
// as it needs attached controller
let hasDescendants = compRef.instance.descendants && compRef.instance.descendants.length;
if (!this.disableLazy && (hasDescendants || compRef.instance._hasSubSchemas)) {
this._loadAfterSelf();
return;
}
insertAfter($element.cloneNode(true), this.elementRef.nativeElement);
this.loaded = true;
} else {
cache[this.pointer] = this._loadAfterSelf();
}
}
projectComponentInputs(instance:JsonSchema) {
Object.assign(instance, this);
}
ngOnInit() {
if (!this.absolutePointer) this.absolutePointer = this.pointer;
}
ngAfterViewInit() {
if (!this.auto && !this.disableLazy) return;
this.loadCached();
}
ngOnDestroy() {
// clear cache
cache = {};
}
}
function insertAfter(newNode, referenceNode) {
referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling);
}

View File

@ -1,107 +0,0 @@
<ng-container [ngSwitch]="schema._widgetType">
<ng-template ngSwitchCase="file">
<span class="param-wrap">
<span class="param-type-file">file</span>
<div *ngIf="schema._produces && !isRequestSchema" class="file produces">
<ul>
<li *ngFor="let type of schema._produces">{{type}}</li>
</ul>
</div>
<div *ngIf="schema._consumes && isRequestSchema" class="file consume">
<ul>
<li *ngFor="let type of schema._consumes">{{type}}</li>
</ul>
</div>
</span>
</ng-template>
<ng-template ngSwitchCase="trivial">
<span class="param-wrap">
<span class="param-type param-type-trivial {{schema.type}}"
[ngClass]="{'with-hint': schema._displayTypeHint, 'array': _isArray}"
title="{{schema._displayTypeHint}}">{{schema._displayType}} {{schema._displayFormat}}
<span class="param-range" *ngIf="schema._range"> {{schema._range}} </span>
</span>
<span *ngIf="schema['x-nullable']" class="param-nullable">Nullable</span>
<div *ngIf="schema.enum" class="param-enum">
<span *ngFor="let enumItem of schema.enum" class="param-enum-value {{enumItem.type}}"> {{enumItem.val | json}} </span>
</div>
<span *ngIf="schema.pattern" class="param-pattern">{{schema.pattern}}</span>
</span>
</ng-template>
<ng-template ngSwitchCase="tuple">
<div class="params-wrap params-array array-tuple">
<ng-template ngFor [ngForOf]="schema.items" let-item="$implicit" let-idx="index" [ngForTrackBy]="trackByIdx">
<div class="tuple-item">
<span class="tuple-item-index"> [{{idx}}]: </span>
<json-schema class="nested-schema" [pointer]="item._pointer"
[absolutePointer]="item._pointer"
[nestOdd]="!nestOdd" [isRequestSchema]="isRequestSchema">
</json-schema>
</div>
</ng-template>
</div>
</ng-template>
<ng-template ngSwitchCase="array">
<json-schema class="nested-schema" [pointer]="schema._pointer"
[nestOdd]="!nestOdd" [isRequestSchema]="isRequestSchema"> </json-schema>
</ng-template>
<ng-template ngSwitchCase="object">
<table class="params-wrap" [ngClass]="{'params-array': _isArray}">
<!-- <caption> {{_displayType}} </caption> -->
<ng-template ngFor [ngForOf]="properties" let-prop="$implicit" let-last="last" [ngForTrackBy]="trackByName">
<tr class="param"
[class.last]="last"
[class.discriminator] = "prop.isDiscriminator"
[class.complex] = "prop._pointer"
[class.additional] = "prop._additional"
[class.expanded] = "subSchema.open">
<td class="param-name">
<span class="param-name-wrap" (click)="subSchema.toggle()">
<span class="param-name-content">
{{prop.name}}
<span class="param-name-enumvalue" [hidden]="!prop._enumItem"> {{prop._enumItem?.val | json}} </span>
</span>
<svg *ngIf="prop._pointer" xmlns="http://www.w3.org/2000/svg" version="1.1" x="0" y="0" viewBox="0 0 24 24" xml:space="preserve">
<polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "/>
</svg>
</span>
</td>
<td class="param-info">
<div>
<span class="param-type {{prop.type}}" [ngClass]="{'with-hint': prop._displayTypeHint, 'tuple': prop._isTuple, 'array': (prop._isArray || prop.type == 'array')}"
title="{{prop._displayTypeHint}}"> {{prop._displayType}} {{prop._displayFormat}}
<span class="param-range" *ngIf="prop._range"> {{prop._range}} </span>
</span>
<span *ngIf="prop._required" class="param-required">Required</span>
<span *ngIf="prop['x-nullable']" class="param-nullable">Nullable</span>
<div class="param-default" *ngIf="prop.default != null">
<span class="param-default-value">{{prop.default | json}}</span>
</div>
<div *ngIf="prop.enum && !prop.isDiscriminator" class="param-enum">
<span *ngFor="let enumItem of prop.enum" class="param-enum-value {{enumItem.type}}"> {{enumItem.val | json}} </span>
</div>
<span *ngIf="prop.pattern" class="param-pattern">{{prop.pattern}}</span>
</div>
<div class="param-description" [innerHtml]="prop.description | marked"></div>
<div class="discriminator-info" *ngIf="prop.isDiscriminator && descendants.length">
<drop-down (change)="selectDescendantByIdx($event)" [active]="activeDescendant.idx">
<option *ngFor="let descendant of descendants; let i=index"
[value]="i" [attr.selected]="descendant.active ? '' : null" >{{descendant.name}}</option>
</drop-down>
</div>
</td>
</tr>
<tr class="param-schema" [ngClass]="{'last': last}" [hidden]="!prop._pointer">
<td colspan="2">
<zippy [attr.disabled]="prop.name" #subSchema title="Expand" [headless]="true" (openChange)="lazySchema.load()" [(open)]="prop.expanded">
<json-schema-lazy #lazySchema [auto]="prop.expanded" class="nested-schema" [pointer]="prop._pointer"
[nestOdd]="!nestOdd" [isRequestSchema]="isRequestSchema" absolutePointer="{{absolutePointer}}/properties/{{prop.name}}">
</json-schema-lazy>
</zippy>
</td>
</tr>
</ng-template>
</table>
</ng-template>
</ng-container>

View File

@ -1,221 +0,0 @@
@import 'json-schema-common';
// styles for array-schema for array
$array-marker-font-sz: 13px;
$array-marker-line-height: 1.5;
:host {
display: block;
}
.param-schema > td {
border-left: $line-border;
padding: 0 10px;
}
.derived-schema {
display: none;
}
.derived-schema.active {
display: block;
}
:host.nested-schema {
background-color: white;
padding: 10px 20px;
position: relative;
border-radius: $border-radius;
&:before, &:after {
content: "";
width: 0;
height: 0;
position: absolute;
top: 0;
border-style: solid;
border-color: transparent;
border-width: 10px 15px 0;
margin-left: -7.5px;
border-top-color: $side-menu-active-bg-color;
}
&:before {
left: 10%;
}
&:after {
right: 10%;
}
.param:first-of-type > .param-name:before, .param:last-of-type > .param-name:after {
border-color: white;
}
}
:host[nestodd="true"] {
background-color: $side-menu-active-bg-color;
border-radius: $border-radius;
&:before, &:after {
border-top-color: white;
}
> .params-wrap > .param:first-of-type > .param-name:before,
> .params-wrap > .param:last-of-type > .param-name:after {
border-color: $side-menu-active-bg-color;
}
> .params-wrap > .param:last-of-type,
> .params-wrap > .param.last {
> .param-name:after {
border-color: $side-menu-active-bg-color;
}
}
}
zippy {
overflow: visible;
}
.zippy-content-wrap {
padding: 0;
}
.param.complex.expanded > .param-info {
border-bottom: 0;
}
.param.complex > .param-name .param-name-wrap {
font-weight: bold;
cursor: pointer;
color: $black;
}
.param.complex > .param-name svg {
height: 1.2em;
width: 1.2em;
vertical-align: middle;
transition: all 0.3s ease;
}
.param.complex.expanded > .param-name svg{
transform: rotateZ(-180deg);
}
.param.additional > .param-name {
color: rgba($black, 0.4);
}
.params-wrap {
width: 100%;
}
table {
border-spacing: 0;
}
.params-wrap.params-array:before, .params-wrap.params-array:after {
display: block;
font-weight: $base-font-weight;
color: $black;
font-size: $array-marker-font-sz;
line-height: $array-marker-line-height;
}
.params-wrap.params-array:after {
content: "]";
font-family: monospace;
}
.params-wrap.params-array:before {
content: "Array [";
padding-top: 1em;
font-family: monospace;
}
.params-wrap.params-array {
padding-left: $bullet-margin;
}
.param-schema.param-array:before {
bottom: ($array-marker-font-sz * $array-marker-line-height) / 2;
width: $bullet-margin;
border-left-style: dashed;
border-bottom: $lines-width dashed $tree-lines-color;
}
.params-wrap.params-array > .param-wrap:first-of-type > .param > .param-name:after {
content: "";
display: block;
position: absolute;
left: -$lines-width;
top: 0;
border-left: $line-border-erase;
height: ($param-name-height/2) + $cell-padding;
}
.params-wrap > .param > .param-schema.param-array {
border-left-color: transparent;
}
.discriminator-info {
margin-top: 5px;
}
.discriminator-wrap:not(.empty) > td {
//border-left: $line-border;
padding: 0;
position: relative;
&:before {
content: "";
display: block;
position: absolute;
left: 0;
top: 0;
border-left: $line-border;
height: ($param-name-height/2) + $cell-padding + 1;
z-index: 1;
}
}
ul, li {
margin: 0;
}
ul {
list-style: none;
padding-left: 1em;
}
li:before {
content: "- ";
font-weight: bold;
}
.array-tuple > .tuple-item {
margin-top: 1.5em;
display: flex;
> span {
flex: 0;
padding: 10px 15px 10px 0;
font-family: monospace;
}
> json-schema {
flex: 1;
&:before, &:after {
display: none;
}
}
}
.param-name-enumvalue {
padding: 2px;
background-color: #e6ebf6;
&:before {
content: " = ";
}
}

View File

@ -1,67 +0,0 @@
'use strict';
import { Component } from '@angular/core';
import {
inject,
TestBed
} from '@angular/core/testing';
import { getChildDebugElement } from '../../../tests/helpers';
import { SpecManager } from '../../utils/spec-manager';;
describe('Redoc components', () => {
beforeEach(() => {
TestBed.configureTestingModule({ declarations: [ TestAppComponent ] });
});
describe('JsonSchema Component', () => {
let builder;
let component;
let fixture;
let specMgr;
beforeEach(inject([SpecManager], ( _spec) => {
specMgr = _spec;
}));
beforeEach(() => {
fixture = TestBed.createComponent(TestAppComponent);
let debugEl = getChildDebugElement(fixture.debugElement, 'json-schema');
component = debugEl.componentInstance;
});
it('should init component', () => {
component.pointer = '';
(<SpecManager>specMgr)._schema = {type: 'object'};
fixture.detectChanges();
expect(component).not.toBeNull();
});
it('should set isTrivial for non-object/array types', () => {
component.pointer = '#';
(<any>specMgr)._schema = {type: 'string'};
fixture.detectChanges();
component.schema.isTrivial.should.be.true();
});
it('should use < anything > notation for prop without type', () => {
component.pointer = '#';
(<any>specMgr)._schema = {type: 'object', properties: {
test: {}
}};
fixture.detectChanges();
component.schema._properties[0]._displayType.should.be.equal('< anything >');
});
});
});
/** Test component that contains a json schema. */
@Component({
selector: 'test-app',
template:
`<json-schema></json-schema>`
})
class TestAppComponent {
}

View File

@ -1,203 +0,0 @@
'use strict';
import { Component,
Input,
Renderer,
ElementRef,
OnInit,
ChangeDetectionStrategy,
ChangeDetectorRef
} from '@angular/core';
import { BaseSearchableComponent, SpecManager } from '../base';
import { SchemaNormalizer, SchemaHelper, AppStateService, OptionsService } from '../../services/';
import { JsonPointer, DescendantInfo } from '../../utils/';
import { Zippy } from '../../shared/components';
import { JsonSchemaLazy } from './json-schema-lazy';
@Component({
selector: 'json-schema',
templateUrl: './json-schema.html',
styleUrls: ['./json-schema.css'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class JsonSchema extends BaseSearchableComponent implements OnInit {
@Input() pointer: string;
@Input() absolutePointer: string;
@Input() final: boolean = false;
@Input() nestOdd: boolean;
@Input() childFor: string;
@Input() isRequestSchema: boolean;
schema: any = {};
activeDescendant:any = {};
discriminator: string = null;
_hasSubSchemas: boolean = false;
properties: any;
_isArray: boolean;
normalizer: SchemaNormalizer;
descendants: DescendantInfo[];
constructor(
specMgr: SpecManager,
app: AppStateService,
private _renderer: Renderer,
private cdr: ChangeDetectorRef,
private _elementRef: ElementRef,
private optionsService: OptionsService) {
super(specMgr, app);
this.normalizer = new SchemaNormalizer(specMgr);
}
get normPointer() {
return this.schema._pointer || this.pointer;
}
selectDescendantByIdx(idx) {
this.selectDescendant(this.descendants[idx]);
}
selectDescendant(activeDescendant: DescendantInfo) {
if (!activeDescendant || activeDescendant.active) return;
this.descendants.forEach(d => {
d.active = false;
});
activeDescendant.active = true;
this.schema = this.specMgr.getDescendant(activeDescendant, this.componentSchema);
this.pointer = this.schema._pointer || activeDescendant.$ref;
this.normalizer.reset();
this.schema = this.normalizer.normalize(this.schema, this.normPointer,
{resolved: true});
this.preprocessSchema();
this.activeDescendant = activeDescendant;
}
initDescendants() {
this.descendants = this.specMgr.findDerivedDefinitions(this.normPointer, this.schema);
if (!this.descendants.length) return;
let discriminator = this.discriminator = this.schema.discriminator || this.schema['x-extendedDiscriminator'];
let discrProperty = this.schema.properties &&
this.schema.properties[discriminator];
if (discrProperty && discrProperty.enum) {
let enumOrder = {};
discrProperty.enum.forEach((enumItem, idx) => {
enumOrder[enumItem] = idx;
});
this.descendants = this.descendants
.filter(a => {
return enumOrder[a.name] != undefined;
}).sort((a, b) => {
return enumOrder[a.name] > enumOrder[b.name] ? 1 : -1;
});
}
this.descendants.forEach((d, idx) => d.idx = idx);
this.selectDescendantByIdx(0);
}
init() {
if (!this.pointer) return;
if (!this.absolutePointer) this.absolutePointer = this.pointer;
this.schema = this.componentSchema;
if (!this.schema) {
throw new Error(`Can't load component schema at ${this.pointer}`);
}
this.applyStyling();
this.schema = this.normalizer.normalize(this.schema, this.normPointer, {resolved: true});
this.schema = SchemaHelper.unwrapArray(this.schema, this.normPointer);
this._isArray = this.schema._isArray;
this.absolutePointer += (this._isArray ? '/items' : '');
this.initDescendants();
this.preprocessSchema();
}
preprocessSchema() {
SchemaHelper.preprocess(this.schema, this.normPointer, this.pointer);
if (!this.schema.isTrivial) {
SchemaHelper.preprocessProperties(this.schema, this.normPointer, {
childFor: this.childFor,
discriminator: this.discriminator
});
}
this.properties = this.schema._properties || [];
if (this.isRequestSchema) {
this.properties = this.properties.filter(prop => !prop.readOnly);
}
if (this.optionsService.options.requiredPropsFirst) {
SchemaHelper.moveRequiredPropsFirst(this.properties, this.schema.required);
}
this._hasSubSchemas = this.properties && this.properties.some(
propSchema => {
if (propSchema.type === 'array') {
propSchema = propSchema.items;
}
return (propSchema && propSchema.type === 'object' && propSchema._pointer);
});
if (this.properties.length === 1) {
this.properties[0].expanded = true;
}
}
applyStyling() {
if (this.nestOdd) {
this._renderer.setElementAttribute(this._elementRef.nativeElement, 'nestodd', 'true');
}
}
trackByName(_: number, item: any): number {
return item.name + (item._pointer || '');
}
trackByIdx(idx: number, _: any): number {
return idx;
}
findDescendantWithField(fieldName: string): DescendantInfo {
let res: DescendantInfo;
for (let descendantInfo of this.descendants) {
let schema = this.specMgr.getDescendant(descendantInfo, this.schema);
this.normalizer.reset();
schema = this.normalizer.normalize(schema, this.normPointer,
{resolved: true});
if (schema.properties && schema.properties[fieldName]) {
res = descendantInfo;
break;
};
};
return res;
}
ensureSearchIsShown(ptr: string) {
if (ptr.startsWith(this.absolutePointer)) {
let props = this.properties;
if (!props) return;
let relative = JsonPointer.relative(this.absolutePointer, ptr);
let propName;
if (relative.length > 1 && relative[0] === 'properties') {
propName = relative[1];
}
let prop = props.find(p => p.name === propName);
if (!prop) {
let d = this.findDescendantWithField(propName);
this.selectDescendant(d);
prop = this.properties.find(p => p.name === propName);
}
if (prop && !prop.isTrivial) prop.expanded = true;
this.cdr.markForCheck();
this.cdr.detectChanges();
}
}
ngOnInit() {
this.preinit();
}
}

View File

@ -1,21 +0,0 @@
:host {
position: fixed;
top: 0;
left: 0;
right: 0;
display: block;
height: 5px;
z-index: 100;
}
span {
display: block;
position: absolute;
left: 0;
top: 0;
bottom: 0;
right: attr(progress percentage);
background-color: #5f7fc3;
transition: right 0.2s linear;
}

View File

@ -1,58 +0,0 @@
'use strict';
import {
Component
} from '@angular/core';
import {
ComponentFixture,
inject,
fakeAsync,
tick,
TestBed,
} from '@angular/core/testing';
import { getChildDebugElement } from '../../../tests/helpers';
import { LoadingBar } from './loading-bar';
describe('Redoc components', () => {
describe('Loading Bar', () => {
let component: LoadingBar;
it('should init component', () => {
let fixture = TestBed.createComponent(LoadingBar);
component = fixture.componentInstance;
fixture.detectChanges();
should.exist(component);
component.progress.should.be.equal(0);
component.display.should.be.equal('block');
});
it('should hide itself in 500ms if progress is 100', fakeAsync(() => {
TestBed.configureTestingModule({ declarations: [ TestAppComponent ] });
let fixture = TestBed.createComponent(TestAppComponent);
let parentComp = fixture.componentInstance;
component = getChildDebugElement(fixture.debugElement, 'loading-bar').componentInstance;
// need to pass update through parent component as ngOnChanges is run only for view changes
parentComp.progress = 50;
fixture.detectChanges();
parentComp.progress = 100;
fixture.detectChanges();
component.display.should.be.equal('block');
tick(500);
component.display.should.be.equal('none');
}));
});
});
/** Test component that contains an ApiInfo. */
@Component({
selector: 'test-app',
template:
`<loading-bar [progress]="progress"></loading-bar>`
})
class TestAppComponent {
progress = 0;
}

View File

@ -1,22 +0,0 @@
'use strict';
import { Input, HostBinding, Component, OnChanges } from '@angular/core';
@Component({
selector: 'loading-bar',
template: `
<span [style.width]='progress + "%"'> </span>
`,
styleUrls: ['loading-bar.css']
})
export class LoadingBar implements OnChanges {
@Input() progress:number = 0;
@HostBinding('style.display') display = 'block';
ngOnChanges(ch) {
if (ch.progress.currentValue === 100) {
setTimeout(() => {
this.display = 'none';
}, 500);
}
}
}

View File

@ -1,32 +0,0 @@
<div class="operation" *ngIf="operation">
<div class="operation-content">
<h2 class="operation-header sharable-header" [class.deprecated]="operation.deprecated">
<a class="share-link" href="#{{operation.anchor}}"></a>{{operation.summary}}
</h2>
<endpoint-link *ngIf="pathInMiddlePanel"
[verb]="operation.verb" [path]="operation.path"> </endpoint-link>
<div class="operation-tags" *ngIf="operation.info.tags.length">
<a *ngFor="let tag of operation.info.tags" attr.href="#tag/{{tag}}"> {{tag}} </a>
</div>
<p *ngIf="operation.info.description" class="operation-description"
[innerHtml]="operation.info.description | marked">
</p>
<redoc-externalDocs [docs]="operation.externalDocs"></redoc-externalDocs>
<params-list pointer="{{pointer}}/parameters"> </params-list>
<responses-list pointer="{{pointer}}/responses"> </responses-list>
</div>
<div class="operation-samples">
<endpoint-link *ngIf="!pathInMiddlePanel"
[verb]="operation.verb" [path]="operation.path"> </endpoint-link>
<div>
<request-samples [pointer]="pointer" [schemaPointer]="operation.bodyParam?._pointer">
</request-samples>
</div>
<div>
<br>
<responses-samples pointer="{{pointer}}/responses"> </responses-samples>
</div>
</div>
</div>

View File

@ -1,148 +0,0 @@
@import '../../shared/styles/variables';
:host {
padding-bottom: 100px;
display: block;
border-bottom: 1px solid rgba(127, 127, 127, 0.25);
margin-top: 1em;
transform: translateZ(0);
z-index: 2;
}
// :host:last-of-type {
// border-bottom: 0;
// }
.operation-header {
margin-bottom: calc(1em - 6px);
&.deprecated {
&:after {
content: 'Deprecated';
display: inline-block;
padding: 0 5px;
margin: 0;
background-color: $yellow;
color: white;
font-weight: bold;
font-size: 13px;
vertical-align: text-top;
}
}
}
.operation-tags {
margin-top: 20px;
> a {
font-size: 16px;
color: #999;
display: inline-block;
padding: 0 0.5em;
text-decoration: none;
&:before {
content: '#';
margin-right: -0.4em;
}
&:first-of-type {
padding: 0;
}
}
}
.operation-content, .operation-samples {
display: block;
box-sizing: border-box;
float: left;
}
.operation-content {
width: 100% - $samples-panel-width;
padding: $section-spacing;
}
.operation-samples {
color: $sample-panel-color;
width: 40%;
padding: $section-spacing;
background: $samples-panel-bg-color;
}
.operation-samples pre {
color: $sample-panel-color;
}
.operation-samples header,
.operation-samples > h5 {
color: $sample-panel-headers-color;
text-transform: uppercase;
}
.operation-samples > h5 {
margin-bottom: 8px;
}
.operation-samples schema-sample {
display: block;
}
.operation:after {
content: "";
display: table;
clear:both;
}
.operation-description {
padding: 6px 0 10px 0;
margin: 0;
}
[select-on-click] {
cursor: pointer;
}
@media (max-width: $right-panel-squash-breakpoint) {
.operations:before {
display: none;
}
.operation-samples, .operation-content {
width: 100%;
}
.operation-samples {
margin-top: 2em;
}
:host {
padding-bottom: 0;
}
}
.operation-content /deep/ endpoint-link {
margin-bottom: 16px;
.operation-endpoint[class] {
padding: 5px 30px 5px 5px;
border: 0;
border-bottom: 1px solid $border-color;
border-radius: 0;
background-color: transparent;
}
.operation-api-url-path {
color: $black;
}
.expand-icon {
top: 8px;
background-color: $border-color;
}
.servers-overlay {
border: 1px solid $border-color;
border-top: 0;
}
}

View File

@ -1,65 +0,0 @@
'use strict';
import { Component } from '@angular/core';
import {
inject,
async,
TestBed
} from '@angular/core/testing';
import { getChildDebugElement } from '../../../tests/helpers';
import { Operation } from './operation';
import { SpecManager } from '../../utils/spec-manager';;
import { LazyTasksService } from '../../shared/components/LazyFor/lazy-for';;
describe('Redoc components', () => {
beforeEach(() => {
TestBed.configureTestingModule({ declarations: [ TestAppComponent ] });
});
describe('Operation Component', () => {
let builder;
let component: Operation;
let specMgr;
beforeEach(async(inject([SpecManager, LazyTasksService], (_specMgr, lazyTasks) => {
lazyTasks.sync = true;
specMgr = _specMgr;
})));
beforeEach(done => {
specMgr.load('/tests/schemas/extended-petstore.yml').then(done, done.fail);
});
beforeEach(() => {
let fixture = TestBed.createComponent(TestAppComponent);
component = getChildDebugElement(fixture.debugElement, 'operation').componentInstance;
fixture.detectChanges();
});
it('should init component', () => {
expect(component).not.toBeNull();
});
it('should init basic component data', () => {
component.operation.verb.should.be.equal('put');
component.operation.path.should.be.equal('/user/{username}');
});
it('should main tag', () => {
component.operation.info.tags.should.be.empty();
});
});
});
/** Test component that contains a Operation. */
@Component({
selector: 'test-app',
template:
`<operation pointer='#/paths/~1user~1{username}/put'></operation>`
})
class TestAppComponent {
}

View File

@ -1,89 +0,0 @@
'use strict';
import { Input, HostBinding, Component, OnInit, ChangeDetectionStrategy, ElementRef } from '@angular/core';
import JsonPointer from '../../utils/JsonPointer';
import { BaseComponent, SpecManager } from '../base';
import { SchemaHelper } from '../../services/schema-helper.service';
import { OptionsService, MenuService } from '../../services/';
import { SwaggerBodyParameter } from '../../utils/swagger-typings';
export interface OperationInfo {
deprecated: boolean;
verb: string;
path: string;
info: {
tags: string[];
description: string;
};
bodyParam: any;
summary: string;
anchor: string;
externalDocs?: {
url: string;
description?: string;
}
}
@Component({
selector: 'operation',
templateUrl: './operation.html',
styleUrls: ['./operation.css'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class Operation extends BaseComponent implements OnInit {
@Input() pointer :string;
@Input() parentTagId :string;
@HostBinding('attr.operation-id') operationId;
operation: OperationInfo;
pathInMiddlePanel: boolean;
constructor(
specMgr:SpecManager,
private optionsService: OptionsService,
private menu: MenuService) {
super(specMgr);
this.pathInMiddlePanel = optionsService.options.pathInMiddlePanel;
}
init() {
this.operationId = this.componentSchema.operationId;
this.operation = {
deprecated: this.componentSchema.deprecated,
verb: JsonPointer.baseName(this.pointer),
path: JsonPointer.baseName(this.pointer, 2),
info: {
description: this.componentSchema.description,
tags: this.filterMainTags(this.componentSchema.tags)
},
bodyParam: this.findBodyParam(),
summary: SchemaHelper.operationSummary(this.componentSchema),
anchor: this.buildAnchor(),
externalDocs: this.componentSchema.externalDocs
};
}
buildAnchor():string {
return this.menu.hashFor(this.pointer,
{ type: 'operation', operationId: this.operationId, pointer: this.pointer },
this.parentTagId );
}
filterMainTags(tags) {
var tagsMap = this.specMgr.getTagsMap();
if (!tags) return [];
return tags.filter(tag => tagsMap[tag] && tagsMap[tag]['x-traitTag']);
}
findBodyParam():SwaggerBodyParameter {
let params = this.specMgr.getOperationParams(this.pointer);
let bodyParam = params.find(param => param.in === 'body');
return bodyParam;
}
ngOnInit() {
this.preinit();
}
}

View File

@ -1,12 +0,0 @@
<div class="operations">
<div class="tag" *ngFor="let tag of tags; trackBy:trackByTagName" [attr.section]="tag.id">
<div class="tag-info" *ngIf="tag.name">
<h1 class="sharable-header"> <a class="share-link" href="#{{tag.anchor}}"></a>{{tag.name}} </h1>
<p *ngIf="tag.description" [innerHtml]="tag.description | marked"> </p>
<redoc-externalDocs [docs]="tag.metadata.externalDocs"></redoc-externalDocs>
</div>
<operation *lazyFor="let operation of tag.items; let ready = ready;"
[hidden]="!ready" [pointer]="operation.metadata.pointer"
[parentTagId]="tag.id" [attr.section]="operation.id"></operation>
</div>
</div>

View File

@ -1,37 +0,0 @@
@import '../../shared/styles/variables';
:host {
display: block;
overflow: hidden;
}
:host [hidden] {
display: none;
}
.tag-info {
padding: $section-spacing;
box-sizing: border-box;
width: 60%;
@media (max-width: $right-panel-squash-breakpoint) {
width: 100%;
}
}
.tag-info:after, .tag-info:before {
content: "";
display: table;
}
.tag-info h1 {
color: $headers-color;
text-transform: capitalize;
font-weight: normal;
margin-top: 0;
}
.operations {
display: block;
position: relative;;
}

View File

@ -1,62 +0,0 @@
'use strict';
import { Component } from '@angular/core';
import {
inject,
async,
TestBed
} from '@angular/core/testing';
import { getChildDebugElement } from '../../../tests/helpers';
import { OperationsList } from './operations-list';
import { SpecManager } from '../../utils/spec-manager';
describe('Redoc components', () => {
beforeEach(() => {
TestBed.configureTestingModule({ declarations: [ TestAppComponent ] });
});
describe('OperationsList Component', () => {
let builder;
let component;
let fixture;
let specMgr;
beforeEach(async(inject([SpecManager], (_specMgr) => {
specMgr = _specMgr;
})));
beforeEach(done => {
specMgr.load('/tests/schemas/operations-list-component.json').then(done, done.fail);
});
beforeEach(() => {
fixture = TestBed.createComponent(TestAppComponent);
component = getChildDebugElement(fixture.debugElement, 'operations-list').componentInstance;
fixture.detectChanges();
});
it('should init component and component data', () => {
expect(component).not.toBeNull();
});
it('should build correct tags list', () => {
expect(component.tags).not.toBeNull();
component.tags.should.have.lengthOf(2);
component.tags[0].name.should.be.equal('traitTag');
should.not.exist(component.tags[0].items);
component.tags[1].name.should.be.equal('tag1');
component.tags[1].items.should.have.lengthOf(2);
});
});
});
@Component({
selector: 'test-app',
template:
`<operations-list></operations-list>`
})
class TestAppComponent {
}

View File

@ -1,57 +0,0 @@
'use strict';
import { Component, Input, OnInit, ChangeDetectionStrategy } from '@angular/core';
import { BaseComponent, SpecManager } from '../base';
import { MenuService } from '../../services/index';
@Component({
selector: 'operations-list',
templateUrl: './operations-list.html',
styleUrls: ['./operations-list.css'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class OperationsList extends BaseComponent implements OnInit {
@Input() pointer:string;
tags:Array<any> = [];
constructor(specMgr:SpecManager, private menu: MenuService) {
super(specMgr);
}
init() {
let flatMenuItems = this.menu.flatItems;
this.tags = [];
let emptyTag = {
name: '',
items: []
};
flatMenuItems.forEach(menuItem => {
// skip items that are not bound to swagger tags/operations
if (!menuItem.metadata) return;
if (menuItem.metadata.type === 'tag') {
this.tags.push({
...menuItem,
anchor: this.buildAnchor(menuItem.id)
});
}
if (menuItem.metadata.type === 'operation' && !menuItem.parent) {
emptyTag.items.push(menuItem);
}
});
if (emptyTag.items.length) this.tags.push(emptyTag);
}
buildAnchor(tagId):string {
return this.menu.hashFor(tagId,
{ type: 'tag'});
}
trackByTagName(_, el) {
return el.name;
}
ngOnInit() {
this.preinit();
}
}

View File

@ -1,53 +0,0 @@
<h5 class="param-list-header" *ngIf="params.length"> Parameters </h5>
<ng-template ngFor [ngForOf]="params" let-paramType="$implicit">
<header class="paramType">
{{paramType.place}} Parameters
<span class="hint--top-right hint--large" [attr.data-hint]="paramType.placeHint">?</span>
</header>
<div class="params-wrap">
<div *ngFor="let param of paramType.params" class="param">
<div class="param-name">
<span class="param-name-wrap"> {{param.name}} </span>
</div>
<div class="param-info">
<div>
<span *ngIf='param.type === "array"'
class="param-array-format param-collection-format-{{param.collectionFormat}}">
{{param | collectionFormat}}
</span>
<span class="param-type {{param.type}}" [ngClass]="{'with-hint': param._displayTypeHint}"
title="{{param._displayTypeHint}}"> {{param._displayType}} {{param._displayFormat}}</span>
<span class="param-range" *ngIf="param._range"> {{param._range}} </span>
<span *ngIf="param.required" class="param-required">Required</span>
<div class="param-default" *ngIf="param.default != null">
<span class="param-default-value">{{param.default | json}}</span>
</div>
<div class="param-example" *ngIf="param.example != null">
<span class="param-example-value">{{param.example | json}}</span>
</div>
<div *ngIf="param.enum || param._enumItem" class="param-enum">
<span *ngFor="let enumItem of param.enum" class="param-enum-value {{enumItem.type}}">
{{enumItem.val | json}}
</span>
<span *ngIf="param._enumItem" class="param-enum-value {{param._enumItem.type}}">
{{param._enumItem.val | json}}
</span>
</div>
<span *ngIf="param.pattern" class="param-pattern">{{param.pattern}}</span>
</div>
<div class="param-description" [innerHtml]="param.description | marked"></div>
</div>
</div>
</div>
</ng-template>
<div *ngIf="bodyParam">
<h5 class="param-list-header" *ngIf="bodyParam"> Request Body </h5>
<div class="body-param-description" [innerHtml]="bodyParam.description | marked"></div>
<div>
<br>
<json-schema-lazy [isRequestSchema]="true" [auto]="true" pointer="{{bodyParam._pointer}}/schema">
</json-schema-lazy>
</div>
</div>

View File

@ -1,99 +0,0 @@
@import '../../shared/styles/variables';
$hint-color: #999999;
:host {
display: block;
}
.param-list-header {
border-bottom: 1px solid rgba($text-color, .3);
// padding: 0.2em 0;
margin: 3em 0 1em 0;
color: rgba($text-color, .5);
font-weight: normal;
text-transform: uppercase;
}
@import '../JsonSchema/json-schema-common';
header.paramType {
margin: 25px 0 5px 0;
text-transform: capitalize;
}
.param-array-format {
color: black;
font-weight: 300;
}
// paramters can't be multilevel so table representation works for it without javascript
.params-wrap {
display: table;
width: 100%;
}
.param-name {
display: table-cell;
vertical-align: top;
}
.param-info {
display: table-cell;
width: 75%;
}
.param {
display: table-row;
}
.param:last-of-type > .param-name {
border-left: 0;
&:after {
content: "";
display: block;
position: absolute;
left: 0;
border-left: $line-border;
height: ($param-name-height/2) + $cell-padding + $lines-width;
background-color: white;
top: 0;
}
}
.param:first-of-type .param-name:after {
content: "";
display: block;
position: absolute;
left: -$lines-width;
border-left: $line-border-erase;
height: ($param-name-height/2) + $cell-padding;
background-color: white;
top: 0;
}
[data-hint] {
width: 1.2em;
text-align: center;
border-radius: 50%;
vertical-align: middle;
color: $hint-color;
line-height: 1.2;
text-transform: none;
cursor: help;
border: 1px solid $hint-color;
margin-left: 0.5em;
}
@media (max-width: 520px) {
[data-hint] {
float: right;
}
[data-hint]:after {
margin-left: 12px;
transform: translateX(-100%) translateY(-8px);
-moz-transform: translateX(-100%) translateY(-8px);
-webkit-transform: translateX(-100%) translateY(-8px);
}
}

View File

@ -1,88 +0,0 @@
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { OptionsService } from '../../services/options.service';
import { SchemaHelper } from '../../services/schema-helper.service';
import { BaseComponent, SpecManager } from '../base';
function safePush(obj, prop, item) {
if (!obj[prop]) obj[prop] = [];
obj[prop].push(item);
}
@Component({
selector: 'params-list',
templateUrl: './params-list.html',
styleUrls: ['./params-list.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ParamsList extends BaseComponent implements OnInit {
@Input() pointer: string;
params: Array<any>;
empty: boolean;
bodyParam: any;
constructor(specMgr: SpecManager, private options: OptionsService) {
super(specMgr);
}
init() {
this.params = [];
let paramsList = this.specMgr.getOperationParams(this.pointer);
const igrnoredHeaders =
this.specMgr.schema['x-ignoredHeaderParameters'] ||
this.options.options.ignoredHeaderParameters ||
[];
paramsList = paramsList
.map(paramSchema => {
let propPointer = paramSchema._pointer;
if (paramSchema.in === 'body') return paramSchema;
return SchemaHelper.preprocess(paramSchema, propPointer, this.pointer);
})
.filter(param => {
return param.in !== 'header' || igrnoredHeaders.indexOf(param.name) < 0;
});
let paramsMap = this.orderParams(paramsList);
if (paramsMap.body && paramsMap.body.length) {
let bodyParam = paramsMap.body[0];
this.bodyParam = bodyParam;
paramsMap.body = undefined;
}
this.empty = !(Object.keys(paramsMap).length || this.bodyParam);
let paramsPlaces = ['path', 'query', 'formData', 'header', 'body'];
let placeHint = {
path: `Used together with Path Templating, where the parameter value is actually part
of the operation's URL. This does not include the host or base path of the API.
For example, in /items/{itemId}, the path parameter is itemId`,
query: `Parameters that are appended to the URL.
For example, in /items?id=###, the query parameter is id`,
formData: `Parameters that are submitted through a form.
application/x-www-form-urlencoded, multipart/form-data or both are usually
used as the content type of the request`,
header: 'Custom headers that are expected as part of the request'
};
let params = [];
paramsPlaces.forEach(place => {
if (paramsMap[place] && paramsMap[place].length) {
params.push({place: place, placeHint: placeHint[place], params: paramsMap[place]});
}
});
this.params = params;
}
orderParams(params):any {
let res = {};
params.forEach((param) => safePush(res, param.in, param));
return res;
}
ngOnInit() {
this.preinit();
}
}

View File

@ -1,55 +0,0 @@
@import url('//fonts.googleapis.com/css?family=Roboto:300,400,700|Montserrat:400,700');
redoc.loading {
position: relative;
display: block;
min-height: 350px;
}
@keyframes rotate {
0% {
transform: rotate(0deg); }
100% {
transform: rotate(360deg);
}
}
redoc.loading:before {
font-family: Helvetica;
content: "Loading";
font-size: 24px;
text-align: center;
padding-top: 40px;
color: #0033a0;
font-weight: normal;
display: block;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: white;
z-index: 9999;
opacity: 1;
transition: all 0.6s ease-out;
}
redoc.loading:after {
z-index: 10000;
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="512" height="512" viewBox="0 0 512 512"><g></g><path d="M275.682 147.999c0 10.864-8.837 19.661-19.682 19.661v0c-10.875 0-19.681-8.796-19.681-19.661v-96.635c0-10.885 8.806-19.661 19.681-19.661v0c10.844 0 19.682 8.776 19.682 19.661v96.635z" fill="#0033a0"/><path d="M275.682 460.615c0 10.865-8.837 19.682-19.682 19.682v0c-10.875 0-19.681-8.817-19.681-19.682v-96.604c0-10.885 8.806-19.681 19.681-19.681v0c10.844 0 19.682 8.796 19.682 19.682v96.604z" fill="#0033a0"/><path d="M147.978 236.339c10.885 0 19.681 8.755 19.681 19.641v0c0 10.885-8.796 19.702-19.681 19.702h-96.624c-10.864 0-19.661-8.817-19.661-19.702v0c0-10.885 8.796-19.641 19.661-19.641h96.624z" fill="#0033a0"/><path d="M460.615 236.339c10.865 0 19.682 8.755 19.682 19.641v0c0 10.885-8.817 19.702-19.682 19.702h-96.584c-10.885 0-19.722-8.817-19.722-19.702v0c0-10.885 8.837-19.641 19.722-19.641h96.584z" fill="#0033a0"/><path d="M193.546 165.703c7.69 7.66 7.68 20.142 0 27.822v0c-7.701 7.701-20.162 7.701-27.853 0.020l-68.311-68.322c-7.68-7.701-7.68-20.142 0-27.863v0c7.68-7.68 20.121-7.68 27.822 0l68.342 68.342z" fill="#0033a0"/><path d="M414.597 386.775c7.7 7.68 7.7 20.163 0.021 27.863v0c-7.7 7.659-20.142 7.659-27.843-0.062l-68.311-68.26c-7.68-7.7-7.68-20.204 0-27.863v0c7.68-7.7 20.163-7.7 27.842 0l68.291 68.322z" fill="#0033a0"/><path d="M165.694 318.464c7.69-7.7 20.153-7.7 27.853 0v0c7.68 7.659 7.69 20.163 0 27.863l-68.342 68.322c-7.67 7.659-20.142 7.659-27.822-0.062v0c-7.68-7.68-7.68-20.122 0-27.801l68.311-68.322z" fill="#0033a0"/><path d="M386.775 97.362c7.7-7.68 20.142-7.68 27.822 0v0c7.7 7.68 7.7 20.183 0.021 27.863l-68.322 68.311c-7.68 7.68-20.163 7.68-27.843-0.020v0c-7.68-7.68-7.68-20.162 0-27.822l68.322-68.332z" fill="#0033a0"/></svg>');
animation: 2s rotate linear infinite;
width: 50px;
height: 50px;
position: absolute;
content: "";
left: 50%;
margin-left: -25px;
background-size: cover;
top: 75px;
transition: all 0.6s ease-out;
opacity: 1;
}
redoc.loading-remove:before, redoc.loading-remove:after {
opacity: 0;
}

View File

@ -1,30 +0,0 @@
<div class="redoc-error" *ngIf="error">
<h1>Oops... ReDoc failed to render this spec</h1>
<div class='redoc-error-details'>{{error.message}}</div>
</div>
<loading-bar *ngIf="options.lazyRendering" [progress]="loadingProgress"> </loading-bar>
<div class="redoc-wrap" *ngIf="specLoaded && !error">
<div class="background">
<div class="background-actual"> </div>
</div>
<div class="menu-content" sticky-sidebar [disable]="specLoading"
[scrollParent]="options.$scrollParent" [scrollYOffset]="options.scrollYOffset">
<div class="menu-header">
<api-logo> </api-logo>
<redoc-search> </redoc-search>
</div>
<side-menu> </side-menu>
</div>
<div class="api-content">
<warnings></warnings>
<api-info></api-info>
<operations-list> </operations-list>
<footer>
<div class="powered-by-badge">
<a href="https://github.com/Rebilly/ReDoc" title="Swagger-generated API Reference Documentation" target="_blank">
Powered by <strong>ReDoc</strong>
</a>
</div>
</footer>
</div>
</div>

View File

@ -1,362 +0,0 @@
@import '../../shared/styles/variables';
:host {
display: block;
box-sizing: border-box;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
-moz-tap-highlight-color: rgba(0, 0, 0, 0);
-ms-tap-highlight-color: rgba(0, 0, 0, 0);
-o-tap-highlight-color: rgba(0, 0, 0, 0);
tap-highlight-color: rgba(0, 0, 0, 0);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-smoothing: antialiased;
-webkit-osx-font-smoothing: grayscale;
-moz-osx-font-smoothing: grayscale;
osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
-moz-text-size-adjust: 100%;
text-size-adjust: 100%;
-webkit-text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.004);
-ms-text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.004);
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.004);
text-rendering: optimizeSpeed !important;
font-smooth: always;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
text-size-adjust: 100%;
}
.redoc-wrap {
z-index: 0;
position: relative;
overflow: hidden;
font-family: $base-font, $base-font-family;
font-size: $em-size;
line-height: $base-line-height;
color: $text-color;
}
.menu-content {
overflow: hidden;
display: flex;
flex-direction: column;
}
side-menu {
overflow: hidden;
}
[sticky-sidebar] {
width: $side-bar-width;
background-color: $side-bar-bg-color;
overflow-x: hidden;
transform: translateZ(0);
z-index: 75;
@media (max-width: $side-menu-mobile-breakpoint) {
width: 100%;
bottom: auto !important;
}
}
.api-content {
margin-left: $side-bar-width;
z-index: 50;
position: relative;
// height: 100vh;
// overflow: scroll;
top: 0;
@media (max-width: $side-menu-mobile-breakpoint) {
padding-top: 3em;
margin-left: 0;
}
}
.background {
position: absolute;
top: 0;
bottom: 0;
right: 0;
left: $side-bar-width;
z-index: 1;
&-actual {
background: $samples-panel-bg-color;
left: 100% - $samples-panel-width;
right: 0;
top: 0;
bottom: 0;
position: absolute;
}
@media (max-width: $right-panel-squash-breakpoint) {
display: none;
}
}
.redoc-error {
padding: 20px;
text-align: center;
color: $red;
> h2 {
color: $red;
font-size: 40px;
}
}
.redoc-error-details {
max-width: 750px;
margin: 0 auto;
font-size: 18px;
}
/* global menu items styles (search results + menu) */
:host /deep/ {
.menu-item-header > span {
display: inline-block;
vertical-align: middle;
}
.menu-item-header > .operation-type + .menu-item-title {
width: calc(100% - 32px); // 32 = 26px image width + 6px margin left
}
.menu-item-header > .operation-type {
width: 26px;
display: inline-block;
height: 13px;
background-color: #333;
border-radius: 3px;
vertical-align: top;
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAACgCAMAAADZ0KclAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAAZQTFRF////////VXz1bAAAAAJ0Uk5T/wDltzBKAAAA80lEQVR42uSWSwLCIAxEX+5/aa2QZBJw5UIt9QMdRqSPEAAw/TyvqzZf150NzdXL49qreXwXjeqz9bqN1tgJl/KLyaVrrL7K7gx+1vlNMqy+helOO4rfBGYZiEkq1ubQ3DeKvc97Et+d+e01vIZlLZZqb1WNJFd8ZKYsmv4Hh3H2fDgjMUI5WSExjiEZs7rEZ5T+/jQn9lhgsw53j/e9MQtxqPsbZY54M5fNl/MY/f1s7NbRSkYlYjc0KPsWMrmhIU9933ywxDiSE+upYNH8TdusUotllNvcAUzfnE/NC4OSYyklQhpdl9E4Tw0Cm4/G9xBgAO7VCkjWLOMfAAAAAElFTkSuQmCC');
background-repeat: no-repeat;
background-position: 6px 4px;
text-indent: -9000px;
margin-right: 6px;
margin-top: 2px;
&.get {
background-position: 8px -12px;
background-color: $get-color;
}
&.post {
background-position: 6px 4px;
background-color: $post-color;
}
&.put {
background-position: 8px -28px;
background-color: $put-color;
}
&.options {
background-position: 4px -148px;
background-color: $options-color;
}
&.patch {
background-position: 4px -114px;
background-color: $patch-color;
}
&.delete {
background-position: 4px -44px;
background-color: $delete-color;
}
&.basic {
background-position: 5px -79px;
background-color: $basic-color;
}
&.link {
background-position: 4px -131px;
background-color: $link-color;
}
&.head {
background-position: 6px -102px;
background-color: $head-color;
}
}
}
/* global redoc styles */
@for $index from 1 through 5 {
:host /deep/ h#{$index} {
margin-top: 0;
font-family: $headers-font, $headers-font-family;
color: $secondary-color;
font-weight: $headers-font-weight;
line-height: 1.5;
margin-bottom: 0.5em;
}
}
:host /deep/ {
h1 {
font-size: $h1;
color: $headers-color;
}
h2 {
font-size: $h2;
}
h3 {
font-size: $h3;
}
h4 {
font-size: $h4;
}
h5 {
font-size: $h5;
line-height: 20px;
}
p {
font-family: $base-font, $base-font-family;
font-weight: $base-font-weight;
margin: 0;
margin-bottom: 1em;
line-height: $base-line-height;
}
a {
text-decoration: none;
color: $primary-color;
}
p > code {
color: $red;
border: 1px solid rgba(38, 50, 56, 0.1);
}
.hint--inversed {
&:before {
border-top-color: #fff;
}
&:after {
background: #fff;
color: #383838;
}
}
@import '../../shared/styles/share-link';
}
footer {
position: relative;
text-align: right;
padding: 10px $section-spacing;
font-size: 15px;
margin-top: -35px;
color: white;
a {
color: white;
}
strong {
font-size: 18px;
}
}
/* markdown elements */
:host /deep/ .redoc-markdown-block {
pre {
font-family: Courier, monospace;
white-space: pre-wrap;
background-color: #263238;
color: white;
padding: 12px 14px 15px 14px;
overflow-x: auto;
line-height: normal;
border-radius: $border-radius;
border: 1px solid rgba(38, 50, 56, 0.1);
code {
background-color: transparent;
color: white;
&:before,
&:after {
content: none;
}
}
}
code {
font-family: Courier, monospace;
background-color: rgba(38, 50, 56, 0.04);
padding: 0.1em 0.2em 0.2em;
font-size: 1em;
border-radius: $border-radius;
color: $red;
border: 1px solid rgba(38, 50, 56, 0.1);
}
p:last-of-type {
margin-bottom: 0;
}
blockquote {
margin: 0;
margin-bottom: 1em;
padding: 0 15px;
color: #777;
border-left: 4px solid #ddd;
}
img {
max-width: 100%;
box-sizing: content-box;
}
ul,
ol {
padding-left: 2em;
margin: 0;
margin-bottom: 1em;
font-family: $base-font, $base-font-family;
font-weight: $base-font-weight;
line-height: $base-line-height;
> li {
margin: 1em 0;
}
}
table {
display: block;
width: 100%;
overflow: auto;
word-break: normal;
word-break: keep-all;
border-collapse: collapse;
border-spacing: 0;
margin-top: 0.5em;
margin-bottom: 0.5em;
}
table tr {
background-color: #fff;
border-top: 1px solid #ccc;
&:nth-child(2n) {
background-color: #f8f8f8;
}
}
table th,
table td {
padding: 6px 13px;
border: 1px solid #ddd;
}
table th {
text-align: left;
font-weight: bold;
}
}

View File

@ -1,60 +0,0 @@
'use strict';
import { getChildDebugElement } from '../../../tests/helpers';
import { Component } from '@angular/core';
import {
inject,
async
} from '@angular/core/testing';
import { TestBed } from '@angular/core/testing';
import { Redoc } from './redoc';
import { SpecManager } from '../../utils/spec-manager';
import { OptionsService } from '../../services/index';
let optsMgr:OptionsService;
describe('Redoc components', () => {
beforeEach(() => {
TestBed.configureTestingModule({ declarations: [ TestAppComponent ] });
});
describe('Redoc Component', () => {
let builder;
let specMgr;
beforeEach(async(inject([SpecManager, OptionsService],
( _specMgr, _optsMgr) => {
optsMgr = _optsMgr;
specMgr = _specMgr;
})));
beforeEach(done => {
specMgr.load('/tests/schemas/extended-petstore.yml').then(done, done.fail);
})
it('should init component', () => {
let fixture = TestBed.createComponent(TestAppComponent);
let component = getChildDebugElement(fixture.debugElement, 'redoc').componentInstance;
expect(component).not.toBeNull();
fixture.destroy();
});
it('should init components tree without errors', () => {
let fixture = TestBed.createComponent(TestAppComponent);
(() => fixture.detectChanges()).should.not.throw();
fixture.destroy();
});
});
});
/** Test component that contains a Redoc. */
@Component({
selector: 'test-app',
template:
`<redoc disable-lazy-schemas></redoc>`
})
class TestAppComponent {
}

View File

@ -1,162 +0,0 @@
'use strict';
import { ElementRef,
ChangeDetectorRef,
Input,
Component,
OnInit,
OnDestroy,
HostBinding
} from '@angular/core';
import { BrowserDomAdapter as DOM } from '../../utils/browser-adapter';
import { BaseComponent } from '../base';
import * as detectScollParent from 'scrollparent';
import { SpecManager } from '../../utils/spec-manager';
import {
SearchService,
OptionsService,
Options,
Hash,
AppStateService,
SchemaHelper,
MenuService,
Marker
} from '../../services/';
import { LazyTasksService } from '../../shared/components/LazyFor/lazy-for';
function getPreOptions() {
return Redoc._preOptions || {};
}
@Component({
selector: 'redoc',
templateUrl: './redoc.html',
styleUrls: ['./redoc.css'],
providers: [
SpecManager,
MenuService,
SearchService,
LazyTasksService,
Marker
]
//changeDetection: ChangeDetectionStrategy.OnPush
})
export class Redoc extends BaseComponent implements OnInit {
static _preOptions: any = {};
error: any;
specLoaded: boolean;
options: Options;
loadingProgress: number;
@Input() specUrl: string;
@HostBinding('class.loading') specLoading: boolean = false;
@HostBinding('class.loading-remove') specLoadingRemove: boolean = false;
private element: HTMLElement;
private $parent: Element;
private $refElem: Element;
constructor(
specMgr: SpecManager,
optionsMgr: OptionsService,
elementRef: ElementRef,
private changeDetector: ChangeDetectorRef,
private appState: AppStateService,
private lazyTasksService: LazyTasksService,
private hash: Hash
) {
super(specMgr);
SchemaHelper.setSpecManager(specMgr);
// merge options passed before init
optionsMgr.options = getPreOptions();
this.element = elementRef.nativeElement;
this.$parent = this.element.parentNode as Element;
this.$refElem = this.element.nextElementSibling;
//parse options (top level component doesn't support inputs)
optionsMgr.parseOptions( this.element );
let scrollParent = detectScollParent( this.element );
if (scrollParent === (document.scrollingElement || document.documentElement)) scrollParent = window;
optionsMgr.options.$scrollParent = scrollParent;
this.options = optionsMgr.options;
this.lazyTasksService.allSync = !this.options.lazyRendering;
}
hideLoadingAnimation() {
if (this.options.hideLoading) {
return
}
requestAnimationFrame(() => {
this.specLoadingRemove = true;
setTimeout(() => {
this.specLoadingRemove = false;
this.specLoading = false;
}, 400);
});
}
showLoadingAnimation() {
if (this.options.hideLoading) {
return
}
this.specLoading = true;
this.specLoadingRemove = false;
}
load() {
// bunlde spec directly if passsed or load by URL
this.specMgr.load(this.options.spec || this.options.specUrl).catch(err => {
throw err;
});
this.appState.loading.subscribe(loading => {
if (loading) {
this.showLoadingAnimation();
} else {
this.hideLoadingAnimation();
}
});
this.specMgr.spec.subscribe((spec) => {
if (!spec) {
this.appState.startLoading();
} else {
this.specLoaded = true;
this.changeDetector.markForCheck();
this.changeDetector.detectChanges();
setTimeout(() => {
this.hash.start();
});
}
});
}
ngOnInit() {
this.lazyTasksService.loadProgress.subscribe(progress => this.loadingProgress = progress)
this.appState.error.subscribe(_err => {
if (!_err) return;
this.appState.stopLoading();
if (this.loadingProgress === 100) return;
this.error = _err;
this.changeDetector.markForCheck();
});
if (this.specUrl) {
this.options.specUrl = this.specUrl;
}
this.load();
}
ngOnDestroy() {
let $clone = this.element.cloneNode();
this.$parent.insertBefore($clone, this.$refElem);
}
}

View File

@ -1,15 +0,0 @@
<header *ngIf="schemaPointer || samples.length"> Request samples </header>
<schema-sample *ngIf="schemaPointer && !samples.length" [skipReadOnly]="true" [pointer]="schemaPointer"> </schema-sample>
<tabs *ngIf="samples.length" [selected] = "selectedLang" (change)=changeLangNotify($event)>
<tab *ngIf="schemaPointer" tabTitle="JSON">
<schema-sample [pointer]="schemaPointer" [skipReadOnly]="true"> </schema-sample>
</tab>
<tab *ngFor="let sample of samples" [tabTitle]="sample.lang">
<div class="code-sample">
<div class="action-buttons">
<span copy-button [copyText]="sample.source" class="hint--top-left hint--inversed"><a>Copy</a></span>
</div>
<pre [innerHtml]="sample.source | prism:sample.lang"></pre>
</div>
</tab>
</tabs>

View File

@ -1,81 +0,0 @@
@import '../../shared/styles/variables';
:host {
overflow: hidden;
display: block;
}
.action-buttons {
opacity: 0;
transition: opacity 0.3s ease;
transform: translateY(100%);
z-index: 3;
position: relative;
height: 2em;
line-height: 2em;
padding-right: 10px;
text-align: right;
margin-top: -1em;
> span > a {
padding: 2px 10px;
color: #ffffff;
cursor: pointer;
&:hover {
background-color: lighten($black, 15%);
}
}
}
.code-sample:hover > .action-buttons {
opacity: 1;
}
header {
font-family: $headers-font;
font-size: $h5;
text-transform: uppercase;
margin: 0;
color: $sample-panel-headers-color;
text-transform: uppercase;
font-weight: normal;
margin-top: 20px;
}
:host /deep/ > tabs > ul li {
font-family: $headers-font;
font-size: .9em;
border-radius: $border-radius;
margin: 2px 0;
padding: 3px 10px 2px 10px;
line-height: 16px;
color: $sample-panel-headers-color;
&:hover {
background-color: rgba(white, .1);
color: #ffffff;
}
&.active {
background-color: #ffffff;
color: $text-color;
}
}
:host /deep/ tabs ul {
padding-top: 10px;
}
.code-sample pre {
overflow-x: auto;
word-break: break-all;
word-wrap: break-word;
white-space: pre-wrap;
margin-top: 0;
overflow-x: auto;
padding: 20px;
border-radius: 4px;
background-color: #222d32;
margin-bottom: 36px;
}

View File

@ -1,60 +0,0 @@
'use strict';
import { Component, ViewChildren, QueryList, Input,
ChangeDetectionStrategy, OnInit, HostBinding, ElementRef, NgZone } from '@angular/core';
import { Subject } from 'rxjs/Subject';
import { BaseComponent, SpecManager } from '../base';
import JsonPointer from '../../utils/JsonPointer';
import { Tabs } from '../../shared/components/index';
import { AppStateService, ScrollService } from '../../services/index';
@Component({
selector: 'request-samples',
templateUrl: './request-samples.html',
styleUrls: ['./request-samples.css'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class RequestSamples extends BaseComponent implements OnInit {
@Input() pointer:string;
@Input() schemaPointer:string;
@ViewChildren(Tabs) childQuery:QueryList<Tabs>;
@HostBinding('attr.hidden') hidden;
childTabs: Tabs;
selectedLang: Subject<any>;
samples: Array<any>;
constructor(
specMgr:SpecManager,
public appState:AppStateService,
private scrollService: ScrollService,
private el: ElementRef,
private zone: NgZone
) {
super(specMgr);
this.selectedLang = this.appState.samplesLanguage;
}
changeLangNotify(lang) {
let relativeScrollPos = this.scrollService.relativeScrollPos(this.el.nativeElement);
this.selectedLang.next(lang);
// do scroll in the end of VM turn to have it seamless
let subscription = this.zone.onMicrotaskEmpty.subscribe(() => {
this.scrollService.scrollTo(this.el.nativeElement, relativeScrollPos);
subscription.unsubscribe();
});
}
init() {
this.schemaPointer = this.schemaPointer ? JsonPointer.join(this.schemaPointer, 'schema') : null;
this.samples = this.componentSchema['x-code-samples'] || [];
if (!this.schemaPointer && !this.samples.length) this.hidden = true;
}
ngOnInit() {
this.preinit();
}
}

View File

@ -1,26 +0,0 @@
<h2 class="responses-list-header" *ngIf="responses.length"> Responses </h2>
<zippy *ngFor="let response of responses;trackBy:trackByCode" [title]="response.code + ' ' + response.description | marked"
[type]="response.type" [(open)]="response.expanded" [empty]="response.empty" (openChange)="lazySchema.load()">
<div *ngIf="response.headers" class="response-headers">
<header>
Headers
</header>
<div class="header" *ngFor="let header of response.headers">
<div class="header-name"> {{header.name}} </div>
<div class="header-type {{header.type}}"> {{header._displayType}} {{header._displayFormat}}
<span class="header-range" *ngIf="header._range"> {{header._range}} </span>
</div>
<div *ngIf="header.default" class="header-default"> Default: {{header.default}} </div>
<div *ngIf="header.enum" class="header-enum">
<span *ngFor="let enumItem of header.enum" class="enum-value {{enumItem.type}}"> {{enumItem.val | json}} </span>
</div>
<div class="header-description" [innerHtml]="header.description | marked"> </div>
</div>
</div>
<header *ngIf="response.schema">
Response Schema
</header>
<json-schema-lazy [auto]="response.expanded" #lazySchema
pointer="{{response.schema ? response.pointer + '/schema' : null}}">
</json-schema-lazy>
</zippy>

View File

@ -1,61 +0,0 @@
@import '../../shared/styles/variables';
:host {
display: block;
}
.responses-list-header {
font-size: 18px;
padding: 0.2em 0;
margin: 3em 0 1.1em;
color: #253137;
font-weight: normal;
}
:host .zippy-title {
font-family: $headers-font, $headers-font-family;
}
.header-name {
font-weight: bold;
display: inline-block;
}
.header-type {
display: inline-block;
font-weight: bold;
color: #999;
}
header {
font-size: 14px;
font-weight: bold;
text-transform: uppercase;
margin-bottom: 15px;
&:not(:first-child) {
margin-top: 15px;
margin-bottom: 0;
}
}
.header {
margin-bottom: 10px;
}
.header-range {
position: relative;
top: 1px;
margin-right: 6px;
margin-left: 6px;
border-radius: $border-radius;
background-color: rgba($primary-color, 0.1);
padding: 0 4px;
color: rgba($primary-color, 0.7);
}
.header-type.array::before {
content: $array-text;
color: $black;
font-weight: $base-font-weight;
}

View File

@ -1,64 +0,0 @@
'use strict';
import { Component } from '@angular/core';
import {
inject,
async,
TestBed,
ComponentFixture
} from '@angular/core/testing';
import { getChildDebugElement } from '../../../tests/helpers';
import { ResponsesList } from './responses-list';
import { SpecManager } from '../../utils/spec-manager';
describe('Redoc components', () => {
describe('ResponsesList Component', () => {
let builder;
let component: ResponsesList;
let fixture: ComponentFixture<ResponsesList>
let specMgr;
beforeEach(async(inject([SpecManager], (_specMgr) => {
specMgr = _specMgr;
})));
beforeEach(done => {
specMgr.load('/tests/schemas/responses-list-component.json').then(done, done.fail);
});
beforeEach(() => {
fixture = TestBed.createComponent(ResponsesList);
component = fixture.componentInstance;
});
it('should instantiate without errors', () => {
should.exist(component);
});
it('should init repsonses list', () => {
component.pointer = '#/paths/~1test1/get/responses';
fixture.detectChanges();
should.exist(component.responses);
component.responses.should.be.lengthOf(2);
});
it('should not overwrite codes for shared schemas', () => {
component.pointer = '#/paths/~1test1/get/responses';
fixture.detectChanges();
let resp1 = component.responses[0];
let resp2 = component.responses[1];
resp1.code.should.not.be.equal(resp2.code);
});
it('should set type of default as error if other 200-399 response is defined', () => {
component.pointer = '#/paths/~1test2/get/responses';
fixture.detectChanges();
let resp1 = component.responses[1];
resp1.type.should.be.equal('error');
});
});
});

Some files were not shown because too many files have changed in this diff Show More