зеркало из https://github.com/Azure/API-Portal.git
Initial commit
This commit is contained in:
Коммит
0861ef70b9
|
@ -0,0 +1,12 @@
|
|||
*.pfx binary
|
||||
*.ico binary
|
||||
*.svg binary
|
||||
*.jpg binary
|
||||
*.png binary
|
||||
*.exe binary
|
||||
*.eot binary
|
||||
*.woff binary
|
||||
*.woff2 binary
|
||||
*.ttf binary
|
||||
*.otf binary
|
||||
*.cdr binary
|
|
@ -0,0 +1,26 @@
|
|||
name: Azure Static Web Apps CI/CD
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build_and_publish_job:
|
||||
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed')
|
||||
runs-on: ubuntu-latest
|
||||
name: Build and Publish Job
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: npm install
|
||||
- run: npm run publish
|
||||
|
||||
- name: Deploy
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: ./dist/website
|
|
@ -0,0 +1,5 @@
|
|||
.history/
|
||||
.vs/
|
||||
.vscode/
|
||||
dist/
|
||||
node_modules/
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE
|
|
@ -0,0 +1,58 @@
|
|||
# API Catalog
|
||||
|
||||
**API Catalog is an open-source static developer portal builder. You can use it to effortlessly document REST APIs from OpenAPI files, for free.**
|
||||
|
||||
This project is a modified version of the [Azure API Management](https://aka.ms/apimrocks)'s developer portal ([documentation](https://aka.ms/apimdocs/portal), [GitHub](https://aka.ms/apimdevportal)).
|
||||
|
||||
![API Catalog](readme.gif)
|
||||
|
||||
## Step 1: Document your first API with GitHub Pages
|
||||
|
||||
Requirements:
|
||||
|
||||
- OpenAPI file with the definition of your API.
|
||||
|
||||
GitHub Pages let you automate deployments of your API catalog and publish it to the Internet for free. Follow the steps below to document your API.
|
||||
|
||||
1. Fork the GitHub repository.
|
||||
1. Go to the **Options** tab in your repository's **Settings**.
|
||||
1. Change the name of the repository to **[username].github.io** (replace `[username]` with your actual account name) for the site to properly load all the assets. It will be deployed under the URL **https://[username].github.io**, without a URL path suffix. If your repository is part of an organization and not under your individual account, use the organization name in the place of `[username]`.
|
||||
1. Scroll down to the **GitHub Pages** section. Select `gh-pages` as the source branch, leave the default root setting, and select **Save**. Copy the GitHub Pages URL (for example, `https://contoso.github.io/`).
|
||||
1. Go to the **Actions** tab and select **Enable** to enable automated publishing of your site to GitHub Pages.
|
||||
1. Navigate to `data/specs` in your GitHub repository and drag-and-drop an OpenAPI file. Provide a commit title and select **Commit changes**.
|
||||
1. The GitHub Action will automatically trigger on the committed change and publish your website. Once it completes, visit the URL you copied in the step 2. (`https://[username].github.io`) to see the published API catalog.
|
||||
|
||||
## Step 2: Customize the site
|
||||
|
||||
Requirements:
|
||||
|
||||
- Git on your machine. Install it by following [this Git tutorial](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git).
|
||||
- Node.js (LTS version, `v10.15.0` or later) and npm on your machine. Follow [this tutorial](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) to install them.
|
||||
|
||||
Follow the steps below to customize the content of your API catalog with the built-in drag-and-drop visual interface - edit or create pages, change styling, modify configuration, and more.
|
||||
|
||||
1. Clone the forked repository to your local environment.
|
||||
1. Launch the server.
|
||||
1. Open command line and run `npm install` to resolve dependencies.
|
||||
1. Run `npm start` to start a local webserver.
|
||||
1. Open `https://localhost:3000/admin` in a browser to access the administrative interface and make changes. Whenever you make a change, save it by selecting the save button (floppy disk icon) or pressing CTRL+S (Command+S on MacOS). Changes are saved into the `/data/content.json` file. For instructions on customizations and overview of the interface, see [documentation of the Azure API Management's developer portal](https://aka.ms/apimdocs/customizeportal).
|
||||
1. After making the changes, push them to your GitHub repository.
|
||||
1. Run `git add -A` to stage all changes.
|
||||
1. Run `git commit -m "Commit message"` to commit them.
|
||||
1. Run `git push` to push them to GitHub.
|
||||
1. GitHub Action will automatically trigger on the commit in the GitHub's repository. It will build and publish your website into the `gh-pages` branch.
|
||||
1. After the GitHub Action completes, visit the published site at `https://[username].github.io`.
|
||||
|
||||
## Alternative deployment models
|
||||
|
||||
### Deploy to Azure App Service
|
||||
|
||||
If you're looking to have more control over hosting, you can deploy your site to Azure App Service - a fully managed platform for building, deploying, and scaling web applications.
|
||||
|
||||
1. Select the **Deploy to Azure** button at the top of this file.
|
||||
2. Specify existing Azure App Service instance or create a new one.
|
||||
3. ...
|
||||
|
||||
### Deploy elsewhere
|
||||
|
||||
You can also run the publishing step locally and deploy the generated static assets to the hosting solution of choice. To run the publishing step, execute the command `npm run publish` on your local machine.
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"fonts": [
|
||||
{
|
||||
"displayName": "Font Awesome icons",
|
||||
"key": "fonts/default",
|
||||
"variants": [
|
||||
{
|
||||
"file": "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.14.0/webfonts/fa-regular-400.ttf",
|
||||
"style": "normal",
|
||||
"weight": "400"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"displayName": "Material Design icons",
|
||||
"variants": [
|
||||
{
|
||||
"file": "https://cdnjs.cloudflare.com/ajax/libs/material-design-icons/3.0.2/iconfont/MaterialIcons-Regular.ttf",
|
||||
"style": "normal",
|
||||
"weight": "400"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,133 @@
|
|||
{
|
||||
"openapi": "3.0.1",
|
||||
"info": {
|
||||
"title": "Book store API",
|
||||
"description": "Useful API for book tracking and cataloging.",
|
||||
"version": "1.0",
|
||||
"x:thumbnail": "https://www.mediaplace.us/wp-content/uploads/2018/08/Barnes-Noble-logo.png"
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "https://alzasloneuap05.azure-api.net/book-store-api"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"/books": {
|
||||
"get": {
|
||||
"summary": "Get all books",
|
||||
"description": "Get all books",
|
||||
"operationId": "get-all-books",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Book"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"Author": {
|
||||
"required": [
|
||||
"firstName",
|
||||
"lastName"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"firstName": {
|
||||
"type": "string",
|
||||
"description": "Author's first name.",
|
||||
"example": "Rudyard"
|
||||
},
|
||||
"lastName": {
|
||||
"type": "string",
|
||||
"description": "Author's last name.",
|
||||
"example": "Kipling"
|
||||
},
|
||||
"address": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"street": {
|
||||
"type": "string",
|
||||
"example": "7319 Douglas Ave SE"
|
||||
},
|
||||
"city": {
|
||||
"type": "string",
|
||||
"example": "Snoqualmie"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Entity that describes a book author."
|
||||
},
|
||||
"Book": {
|
||||
"required": [
|
||||
"title",
|
||||
"author",
|
||||
"published"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"isbn": {
|
||||
"type": "string",
|
||||
"description": "The International Standard Book Number (ISBN) is a numeric commercial book identifier which is intended to be unique.",
|
||||
"example": "X-XXXXXX-XX-1"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "Book title.",
|
||||
"example": "The Jungle Book"
|
||||
},
|
||||
"author": {
|
||||
"$ref": "#/components/schemas/Author"
|
||||
},
|
||||
"coAuthors": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Author"
|
||||
}
|
||||
},
|
||||
"published": {
|
||||
"type": "number",
|
||||
"description": "Year the book was published.",
|
||||
"example": 1894
|
||||
}
|
||||
},
|
||||
"description": "A book.",
|
||||
"example": {
|
||||
"isbn": "X-XXXXXX-XX-1",
|
||||
"title": "The Jungle book",
|
||||
"author": "Rudyard Kipling",
|
||||
"published": 1894
|
||||
}
|
||||
}
|
||||
},
|
||||
"securitySchemes": {
|
||||
"apiKeyHeader": {
|
||||
"type": "apiKey",
|
||||
"name": "Ocp-Apim-Subscription-Key",
|
||||
"in": "header"
|
||||
},
|
||||
"apiKeyQuery": {
|
||||
"type": "apiKey",
|
||||
"name": "subscription-key",
|
||||
"in": "query"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"apiKeyHeader": []
|
||||
},
|
||||
{
|
||||
"apiKeyQuery": []
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,204 @@
|
|||
{
|
||||
"openapi": "3.0.1",
|
||||
"info": {
|
||||
"title": "httpbin.org",
|
||||
"description": "API Management facade for a very handy and free online HTTP tool.",
|
||||
"version": "1.0"
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "https://httpbin.org"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"/status/{code}": {
|
||||
"get": {
|
||||
"summary": "/status",
|
||||
"description": "Returns provided HTTP Status code.",
|
||||
"operationId": "status",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "code",
|
||||
"in": "path",
|
||||
"description": "HTTP code to return.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"enum": [
|
||||
200
|
||||
],
|
||||
"type": "number",
|
||||
"default": 200
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": null
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/get": {
|
||||
"get": {
|
||||
"summary": "/get",
|
||||
"description": "Returns GET data.",
|
||||
"operationId": "get",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": null
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/post": {
|
||||
"post": {
|
||||
"summary": "/post",
|
||||
"description": "Returns POST data.",
|
||||
"operationId": "post",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": null
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/patch": {
|
||||
"patch": {
|
||||
"summary": "/patch",
|
||||
"description": "Returns PATCH data.",
|
||||
"operationId": "patch",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": null
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/put": {
|
||||
"put": {
|
||||
"summary": "/put",
|
||||
"description": "Returns PUT data.",
|
||||
"operationId": "put",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": null
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/delete": {
|
||||
"delete": {
|
||||
"summary": "/delete",
|
||||
"description": "Returns DELETE data.",
|
||||
"operationId": "delete",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": null
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/xml": {
|
||||
"get": {
|
||||
"summary": "/xml",
|
||||
"description": "Returns some XML.",
|
||||
"operationId": "xml",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": null
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/ip": {
|
||||
"get": {
|
||||
"summary": "/ip",
|
||||
"description": "Returns origin IP.",
|
||||
"operationId": "ip",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": null
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/user-agent": {
|
||||
"get": {
|
||||
"summary": "/user-agent",
|
||||
"description": "Returns user agent string.",
|
||||
"operationId": "user-agent",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": null
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/headers": {
|
||||
"get": {
|
||||
"summary": "/headers",
|
||||
"description": "Returns headers dictionary.",
|
||||
"operationId": "headers",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": null
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/delay/{seconds}": {
|
||||
"get": {
|
||||
"summary": "/delay",
|
||||
"description": "Delays responding for n–10 seconds.",
|
||||
"operationId": "delay",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "seconds",
|
||||
"in": "path",
|
||||
"description": "",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"enum": [
|
||||
"2"
|
||||
],
|
||||
"type": "string",
|
||||
"default": "2"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": null
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/cache/{maxAge}": {
|
||||
"get": {
|
||||
"summary": "/cache",
|
||||
"operationId": "cache",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "maxAge",
|
||||
"in": "path",
|
||||
"description": "",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"enum": [
|
||||
"10"
|
||||
],
|
||||
"type": "string",
|
||||
"default": "10"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {}
|
||||
}
|
|
@ -0,0 +1,177 @@
|
|||
{
|
||||
"openapi": "3.0.0",
|
||||
"info": {
|
||||
"version": "1.0.0",
|
||||
"title": "Swagger Petstore",
|
||||
"description": "Example API thats show cases OpenAPI spec",
|
||||
"x:thumbnail": "https://i.pinimg.com/originals/6a/97/3a/6a973acc6f9e9fb337ba5509bb77e58e.jpg",
|
||||
"license": {
|
||||
"name": "MIT"
|
||||
}
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "http://petstore.swagger.io/v1"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"/pets": {
|
||||
"get": {
|
||||
"summary": "List all pets",
|
||||
"operationId": "listPets",
|
||||
"tags": [
|
||||
"pets"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "limit",
|
||||
"in": "query",
|
||||
"description": "How many items to return at one time (max 100)",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "A paged array of pets",
|
||||
"headers": {
|
||||
"x-next": {
|
||||
"description": "A link to the next page of responses",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Pets"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"description": "unexpected error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"summary": "Create a pet",
|
||||
"operationId": "createPets",
|
||||
"tags": [
|
||||
"pets"
|
||||
],
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Null response"
|
||||
},
|
||||
"default": {
|
||||
"description": "unexpected error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/pets/{petId}": {
|
||||
"get": {
|
||||
"summary": "Info for a specific pet",
|
||||
"operationId": "showPetById",
|
||||
"tags": [
|
||||
"pets"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "petId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "The id of the pet to retrieve",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Expected response to a valid request",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Pets"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"description": "unexpected error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"Pet": {
|
||||
"required": [
|
||||
"id",
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"tag": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Pets": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Pet"
|
||||
}
|
||||
},
|
||||
"Error": {
|
||||
"required": [
|
||||
"code",
|
||||
"message"
|
||||
],
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -0,0 +1,104 @@
|
|||
{
|
||||
"name": "apim-developer-portal",
|
||||
"version": "2.0.0",
|
||||
"description": "API management developer portal",
|
||||
"author": "Microsoft",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"azure"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=10.12"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "webpack --config webpack.designer.js && webpack --config webpack.host.js && node ./dist/host/index.js",
|
||||
"build": "webpack --config webpack.build.js",
|
||||
"build-designer": "webpack --config webpack.designer.js",
|
||||
"build-publisher": "webpack --config webpack.publisher.js",
|
||||
"build-runtime": "webpack --config webpack.runtime.js",
|
||||
"build-host": "webpack --config webpack.host.js",
|
||||
"publish": "webpack --config webpack.publisher.js && node dist/publisher/index.js",
|
||||
"postinstall": "npm rebuild node-sass"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@azure/storage-blob": "12.2.1",
|
||||
"@types/chai": "^4.2.14",
|
||||
"@types/google-maps": "^3.2.2",
|
||||
"@types/knockout": "^3.4.69",
|
||||
"@types/knockout.mapping": "^2.0.35",
|
||||
"@types/knockout.validation": "0.0.37",
|
||||
"@types/lodash": "^4.14.162",
|
||||
"@types/mime-types": "^2.1.0",
|
||||
"@types/mocha": "8.0.3",
|
||||
"@types/node": "^14.11.10",
|
||||
"@types/puppeteer": "^3.0.2",
|
||||
"autoprefixer": "^10.0.1",
|
||||
"awesome-typescript-loader": "^5.2.1",
|
||||
"azure-storage": "^2.10.3",
|
||||
"chai": "^4.2.0",
|
||||
"clean-webpack-plugin": "2.0.2",
|
||||
"copy-webpack-plugin": "^6.2.1",
|
||||
"css-loader": "^5.0.0",
|
||||
"file-loader": "^6.1.1",
|
||||
"html-loader": "^1.3.2",
|
||||
"mini-css-extract-plugin": "^1.0.0",
|
||||
"mocha": "^8.1.3",
|
||||
"node-sass": "^4.14.1",
|
||||
"path": "^0.12.7",
|
||||
"postcss-loader": "^4.0.4",
|
||||
"puppeteer": "^5.3.1",
|
||||
"raw-loader": "^4.0.2",
|
||||
"sass-loader": "^10.0.3",
|
||||
"style-loader": "^2.0.0",
|
||||
"terser-webpack-plugin": "^5.0.0",
|
||||
"ts-node": "9.0.0",
|
||||
"tslint": "^6.1.3",
|
||||
"typescript": "4.0.3",
|
||||
"url-loader": "^4.1.1",
|
||||
"webpack": "^4.44.1",
|
||||
"webpack-cli": "^3.3.12",
|
||||
"webpack-dev-server": "^3.11.0",
|
||||
"webpack-merge": "^5.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@paperbits/azure": "0.1.383",
|
||||
"@paperbits/common": "0.1.383",
|
||||
"@paperbits/core": "0.1.383",
|
||||
"@paperbits/prosemirror": "0.1.383",
|
||||
"@paperbits/styles": "0.1.383",
|
||||
"@types/express": "^4.17.9",
|
||||
"@webcomponents/custom-elements": "1.4.2",
|
||||
"@webcomponents/shadydom": "^1.7.4",
|
||||
"applicationinsights-js": "^1.0.21",
|
||||
"await-parallel-limit": "^2.1.0",
|
||||
"body-parser": "^1.19.0",
|
||||
"class-transformer": "^0.3.1",
|
||||
"class-validator": "^0.12.2",
|
||||
"client-oauth2": "4.3.3",
|
||||
"core-js": "^3.6.5",
|
||||
"cors": "^2.8.5",
|
||||
"d3": "^6.2.0",
|
||||
"express": "^4.17.1",
|
||||
"google-maps": "^4.3.3",
|
||||
"js-beautify": "^1.13.0",
|
||||
"kcors": "^2.2.2",
|
||||
"knockout": "^3.5.1",
|
||||
"knockout-mapping": "^2.6.0",
|
||||
"knockout.validation": "^2.0.4",
|
||||
"liquidjs": "^9.16.1",
|
||||
"lodash": "^4.17.20",
|
||||
"lunr": "^2.3.9",
|
||||
"mime-types": "^2.1.27",
|
||||
"moment": "^2.29.1",
|
||||
"msal": "^1.4.1",
|
||||
"prismjs": "^1.22.0",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"remark": "^13.0.0",
|
||||
"remark-html": "^13.0.1",
|
||||
"routing-controllers": "^0.9.0-alpha.6",
|
||||
"slick": "^1.12.2",
|
||||
"topojson-client": "^3.1.0",
|
||||
"truncate-html": "^1.0.3",
|
||||
"xhr2": "^0.2.0"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
module.exports = {
|
||||
plugins: [
|
||||
require("autoprefixer")
|
||||
],
|
||||
sourceMap: true,
|
||||
minimize: true
|
||||
}
|
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 6.1 MiB |
|
@ -0,0 +1,31 @@
|
|||
import { AccessToken } from "./accessToken";
|
||||
import { HttpHeader } from "@paperbits/common/http/httpHeader";
|
||||
|
||||
export interface IAuthenticator {
|
||||
/**
|
||||
* Returns access token for current session.
|
||||
*/
|
||||
getAccessToken(): Promise<string>;
|
||||
|
||||
/**
|
||||
* Sets new token for the session.
|
||||
* @param accessToken {string} Access token in SharedAccessSignature or Bearer token format.
|
||||
*/
|
||||
setAccessToken(accessToken: AccessToken): Promise<void>;
|
||||
|
||||
/**
|
||||
* Sets new token for the session from response header and return refreshed value
|
||||
* @param responseHeaders {HttpHeader[]} Response headers.
|
||||
*/
|
||||
refreshAccessTokenFromHeader(responseHeaders: HttpHeader[]): Promise<string>;
|
||||
|
||||
/**
|
||||
* Clears access token from current session.
|
||||
*/
|
||||
clearAccessToken(cleanOnlyClient?: boolean): void;
|
||||
|
||||
/**
|
||||
* Checks if current user is signed in.
|
||||
*/
|
||||
isAuthenticated(): Promise<boolean>;
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
import * as moment from "moment";
|
||||
import { Utils } from "../utils";
|
||||
|
||||
export class AccessToken {
|
||||
constructor(
|
||||
/**
|
||||
* Type of token, i.e. Bearer or SharedAccessSignature.
|
||||
*/
|
||||
public readonly type: string,
|
||||
|
||||
/**
|
||||
* Token value.
|
||||
*/
|
||||
public readonly value: string,
|
||||
|
||||
/**
|
||||
* Token expiration date time (UTC).
|
||||
*/
|
||||
public readonly expires: Date,
|
||||
|
||||
/**
|
||||
* User for whom the token was issued.
|
||||
*/
|
||||
public readonly userId?: string) {
|
||||
}
|
||||
|
||||
private static parseExtendedSharedAccessSignature(value: string): AccessToken {
|
||||
const regex = /token=\"(.*==)\"/gm;
|
||||
const match = regex.exec(value);
|
||||
|
||||
if (match && match.length >= 2) {
|
||||
const tokenValue = match[1];
|
||||
|
||||
return AccessToken.parseSharedAccessSignature(tokenValue);
|
||||
}
|
||||
|
||||
throw new Error(`SharedAccessSignature token format is not valid.`);
|
||||
}
|
||||
|
||||
private static parseSharedAccessSignature(value: string): AccessToken {
|
||||
const regex = /^[\w\-]*\&(\d*)\&/gm;
|
||||
const match = regex.exec(value);
|
||||
|
||||
if (!match || match.length < 2) {
|
||||
throw new Error(`SharedAccessSignature token format is not valid.`);
|
||||
}
|
||||
|
||||
const dateTime = match[1];
|
||||
const dateTimeIso = `${dateTime.substr(0, 8)} ${dateTime.substr(8, 4)}`;
|
||||
const expirationDateUtc = moment(dateTimeIso).toDate();
|
||||
|
||||
return new AccessToken("SharedAccessSignature", value, expirationDateUtc);
|
||||
}
|
||||
|
||||
private static parseBearerToken(value: string): AccessToken {
|
||||
const decodedToken = Utils.parseJwt(value);
|
||||
return new AccessToken("Bearer", value, decodedToken.exp);
|
||||
}
|
||||
|
||||
public static parse(token: string): AccessToken {
|
||||
if (!token) {
|
||||
throw new Error("Access token not specified.");
|
||||
}
|
||||
|
||||
if (token.startsWith("Bearer ")) {
|
||||
return AccessToken.parseBearerToken(token.replace("Bearer ", ""));
|
||||
}
|
||||
|
||||
if (token.startsWith("SharedAccessSignature ")) {
|
||||
const value = token.replace("SharedAccessSignature ", "");
|
||||
|
||||
if (value.startsWith("token=")) {
|
||||
return AccessToken.parseExtendedSharedAccessSignature(value);
|
||||
}
|
||||
else {
|
||||
return AccessToken.parseSharedAccessSignature(value);
|
||||
}
|
||||
}
|
||||
|
||||
if (token.startsWith("token=")) {
|
||||
return AccessToken.parseExtendedSharedAccessSignature(token);
|
||||
}
|
||||
|
||||
const result = AccessToken.parseSharedAccessSignature(token);
|
||||
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
throw new Error(`Access token format is not valid. Please use "Bearer" or "SharedAccessSignature".`);
|
||||
}
|
||||
|
||||
public isExpired(): boolean {
|
||||
const utcNow = Utils.getUtcDateTime();
|
||||
|
||||
return utcNow > this.expires;
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return `${this.type} token="${this.value}",refresh="true"`;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
import { SessionManager } from "./sessionManager";
|
||||
|
||||
export class DefaultSessionManager implements SessionManager {
|
||||
public async getItem<T>(key: string): Promise<T> {
|
||||
const value = sessionStorage.getItem(key);
|
||||
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return JSON.parse(value);
|
||||
}
|
||||
|
||||
public async setItem<T>(key: string, value: T): Promise<void> {
|
||||
sessionStorage.setItem(key, JSON.stringify(value));
|
||||
}
|
||||
|
||||
public async removeItem(key: string): Promise<void> {
|
||||
sessionStorage.removeItem(key);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./IAuthenticator";
|
||||
export * from "./accessToken";
|
|
@ -0,0 +1,20 @@
|
|||
export interface SessionManager {
|
||||
/**
|
||||
* Returns stored value.
|
||||
* @param key
|
||||
*/
|
||||
getItem<T>(key: string): Promise<T>;
|
||||
|
||||
/**
|
||||
* Stores value with specified key.
|
||||
* @param key {string} Stored value key.
|
||||
* @param value {T} Stored value.
|
||||
*/
|
||||
setItem<T>(key: string, value: T): Promise<void>;
|
||||
|
||||
/**
|
||||
* Removes value with specified key.
|
||||
* @param key {string} Stored value key.
|
||||
*/
|
||||
removeItem(key: string): Promise<void>;
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
import * as ko from "knockout";
|
||||
|
||||
ko.extenders.acceptChange = (target, condition) => {
|
||||
const result = ko.pureComputed({
|
||||
read: target,
|
||||
write: (newValue) => {
|
||||
if (!ko.unwrap(condition)) {
|
||||
return;
|
||||
}
|
||||
target(newValue);
|
||||
}
|
||||
});
|
||||
|
||||
result(target());
|
||||
|
||||
return result;
|
||||
};
|
|
@ -0,0 +1,25 @@
|
|||
import * as ko from "knockout";
|
||||
|
||||
ko.bindingHandlers["copyToClipboard"] = {
|
||||
init: (element: HTMLElement, valueAccessor: () => string): void => {
|
||||
const copyToClipboard = () => {
|
||||
const placeholder = document.createElement("textarea");
|
||||
placeholder.innerText = ko.unwrap(valueAccessor());
|
||||
document.body.appendChild(placeholder);
|
||||
|
||||
const range = document.createRange();
|
||||
range.selectNode(placeholder);
|
||||
|
||||
const selection = window.getSelection();
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
|
||||
document.execCommand("copy");
|
||||
|
||||
selection.removeAllRanges();
|
||||
document.body.removeChild(placeholder);
|
||||
};
|
||||
|
||||
ko.applyBindingsToNode(element, { click: copyToClipboard }, null);
|
||||
}
|
||||
};
|
|
@ -0,0 +1,48 @@
|
|||
import * as ko from "knockout";
|
||||
import * as remark from "remark";
|
||||
import * as html from "remark-html";
|
||||
import * as truncateHtml from "truncate-html";
|
||||
|
||||
interface MarkdownConfig {
|
||||
/**
|
||||
* Markdown source.
|
||||
*/
|
||||
source: string;
|
||||
|
||||
/**
|
||||
* Maximum length of text before truncation.
|
||||
*/
|
||||
truncateAt: number;
|
||||
}
|
||||
|
||||
|
||||
ko.bindingHandlers["markdown"] = {
|
||||
init: (element: HTMLElement, valueAccessor: () => string | MarkdownConfig): void => {
|
||||
const config = ko.unwrap(valueAccessor());
|
||||
const htmlObservable = ko.observable();
|
||||
|
||||
let markdown: string;
|
||||
let length: number;
|
||||
|
||||
if (!config) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof config === "string") {
|
||||
markdown = config;
|
||||
}
|
||||
else {
|
||||
markdown = config.source;
|
||||
length = config.truncateAt;
|
||||
}
|
||||
|
||||
ko.applyBindingsToNode(element, { html: htmlObservable }, null);
|
||||
|
||||
remark()
|
||||
.use(html)
|
||||
.process(markdown, (err: any, html: any) => {
|
||||
html = truncateHtml.default(html, { length: length, reserveLastWord: true });
|
||||
htmlObservable(html);
|
||||
});
|
||||
}
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
import * as ko from "knockout";
|
||||
|
||||
ko.bindingHandlers["scrollintoview"] = {
|
||||
init: (element: HTMLElement): void => {
|
||||
element.scrollIntoView({ behavior: "smooth", block: "end" });
|
||||
}
|
||||
};
|
|
@ -0,0 +1,86 @@
|
|||
import * as ko from "knockout";
|
||||
import * as Prism from "prismjs";
|
||||
import "prismjs/components/prism-javascript";
|
||||
import "prismjs/components/prism-http";
|
||||
import "prismjs/components/prism-c";
|
||||
import "prismjs/components/prism-csharp";
|
||||
import "prismjs/components/prism-java";
|
||||
import "prismjs/components/prism-python";
|
||||
import "prismjs/components/prism-ruby";
|
||||
import "prismjs/components/prism-markup";
|
||||
import "prismjs/components/prism-bash";
|
||||
import "prismjs/components/prism-json";
|
||||
// import "prismjs/components/prism-php"; // broken!
|
||||
|
||||
|
||||
interface SyntaxHighlightConfig {
|
||||
code: string;
|
||||
language: string;
|
||||
}
|
||||
|
||||
ko.bindingHandlers["syntaxHighlight"] = {
|
||||
update: (element: HTMLElement, valueAccessor: () => SyntaxHighlightConfig): void => {
|
||||
const config = valueAccessor();
|
||||
let code = ko.unwrap(config.code);
|
||||
const language = ko.unwrap(config.language);
|
||||
|
||||
const render = async () => {
|
||||
let highlightLanguage;
|
||||
|
||||
switch (language) {
|
||||
case "csharp":
|
||||
highlightLanguage = "csharp";
|
||||
break;
|
||||
case "curl":
|
||||
highlightLanguage = "bash";
|
||||
break;
|
||||
case "http":
|
||||
highlightLanguage = "http";
|
||||
break;
|
||||
case "java":
|
||||
highlightLanguage = "java";
|
||||
break;
|
||||
case "javascript":
|
||||
highlightLanguage = "js";
|
||||
break;
|
||||
case "objc":
|
||||
highlightLanguage = "c";
|
||||
break;
|
||||
case "php":
|
||||
// highlightLanguage = "php"; // broken!
|
||||
highlightLanguage = "ruby";
|
||||
break;
|
||||
case "python":
|
||||
highlightLanguage = "python";
|
||||
break;
|
||||
case "ruby":
|
||||
highlightLanguage = "ruby";
|
||||
break;
|
||||
case "xml":
|
||||
highlightLanguage = "xml";
|
||||
break;
|
||||
case "json":
|
||||
highlightLanguage = "json";
|
||||
break;
|
||||
default:
|
||||
highlightLanguage = "plain";
|
||||
}
|
||||
|
||||
if (highlightLanguage === "plain") {
|
||||
const text = code;
|
||||
ko.applyBindingsToNode(element, { text: text }, null);
|
||||
}
|
||||
else {
|
||||
|
||||
code = code.replaceAll("/", "fwdslsh"); // workaround for PrismJS bug.
|
||||
let html = Prism.highlight(code, Prism.languages[highlightLanguage], highlightLanguage);
|
||||
html = html.replaceAll("fwdslsh", "/");
|
||||
|
||||
// const html = Prism.highlight(code, Prism.languages[highlightLanguage], highlightLanguage);
|
||||
ko.applyBindingsToNode(element, { html: html }, null);
|
||||
}
|
||||
};
|
||||
|
||||
render();
|
||||
}
|
||||
};
|
|
@ -0,0 +1,9 @@
|
|||
import { Contract } from "@paperbits/common";
|
||||
import { HyperlinkContract } from "@paperbits/common/editing";
|
||||
|
||||
export interface DetailsOfApiContract extends Contract {
|
||||
/**
|
||||
* Link to a page that contains API changlog.
|
||||
*/
|
||||
changeLogPageHyperlink?: HyperlinkContract;
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
import { IWidgetOrder, IWidgetHandler } from "@paperbits/common/editing";
|
||||
import { DetailsOfApiModel } from "./detailsOfApiModel";
|
||||
|
||||
export class DetailsOfApiHandlers implements IWidgetHandler {
|
||||
public async getWidgetOrder(): Promise<IWidgetOrder> {
|
||||
const widgetOrder: IWidgetOrder = {
|
||||
name: "apiDetails",
|
||||
category: "APIs",
|
||||
displayName: "API: details",
|
||||
iconClass: "paperbits-cheque-3",
|
||||
requires: ["html"],
|
||||
createModel: async () => new DetailsOfApiModel()
|
||||
};
|
||||
|
||||
return widgetOrder;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import { HyperlinkModel } from "@paperbits/common/permalinks";
|
||||
|
||||
export class DetailsOfApiModel {
|
||||
/**
|
||||
* Link to a page that contains API changelog.
|
||||
*/
|
||||
public changeLogPageHyperlink: HyperlinkModel;
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
import { Contract } from "@paperbits/common";
|
||||
import { IModelBinder } from "@paperbits/common/editing";
|
||||
import { DetailsOfApiModel } from "./detailsOfApiModel";
|
||||
import { DetailsOfApiContract } from "./detailsOfApiContract";
|
||||
import { IPermalinkResolver } from "@paperbits/common/permalinks";
|
||||
|
||||
|
||||
export class DetailsOfApiModelBinder implements IModelBinder<DetailsOfApiModel> {
|
||||
constructor(private readonly permalinkResolver: IPermalinkResolver) { }
|
||||
|
||||
public canHandleModel(model: Object): boolean {
|
||||
return model instanceof DetailsOfApiModel;
|
||||
}
|
||||
|
||||
public async contractToModel(contract: DetailsOfApiContract): Promise<DetailsOfApiModel> {
|
||||
const model = new DetailsOfApiModel();
|
||||
|
||||
if (contract.changeLogPageHyperlink) {
|
||||
model.changeLogPageHyperlink = await this.permalinkResolver.getHyperlinkFromContract(contract.changeLogPageHyperlink);
|
||||
}
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
public canHandleContract(contract: Contract): boolean {
|
||||
return contract.type === "detailsOfApi";
|
||||
}
|
||||
|
||||
public modelToContract(model: DetailsOfApiModel): Contract {
|
||||
const searchResultConfig: DetailsOfApiContract = {
|
||||
type: "detailsOfApi",
|
||||
changeLogPageHyperlink: model.changeLogPageHyperlink
|
||||
? {
|
||||
target: model.changeLogPageHyperlink.target,
|
||||
targetKey: model.changeLogPageHyperlink.targetKey
|
||||
} : null
|
||||
};
|
||||
|
||||
return searchResultConfig;
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
<api-details data-bind="attr: { params: runtimeConfig }"></api-details>
|
|
@ -0,0 +1,11 @@
|
|||
import { IInjectorModule, IInjector } from "@paperbits/common/injection";
|
||||
import { DetailsOfApiModelBinder } from "../detailsOfApiModelBinder";
|
||||
import { DetailsOfApiViewModelBinder } from "./detailsOfApiViewModelBinder";
|
||||
|
||||
|
||||
export class DetailsOfApiModule implements IInjectorModule {
|
||||
public register(injector: IInjector): void {
|
||||
injector.bindToCollection("modelBinders", DetailsOfApiModelBinder);
|
||||
injector.bindToCollection("viewModelBinders", DetailsOfApiViewModelBinder);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
<fieldset class="form flex-item flex-item-grow" data-bind="scrollable: {}">
|
||||
<div class="form-group">
|
||||
<div class="label-group">
|
||||
<label for="hyperlink" class="form-label">
|
||||
Link to API changelog page
|
||||
</label>
|
||||
<button class="btn btn-info" type="button" title="Help" aria-label="Help"
|
||||
data-bind="tooltipToggle: 'A link to be navigated on click.'"></button>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<input id="hyperlink" class="form-control" readonly data-bind="value: hyperlinkTitle" />
|
||||
<button class="btn input-group-btn" aria-label="Select hyperlink"
|
||||
data-bind="balloon: { position: 'right', component: { name: 'hyperlink-selector', params: { hyperlink: $component.hyperlink, onChange: $component.onHyperlinkChange } } }">
|
||||
<i class="paperbits-icon paperbits-link-69-2"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
|
@ -0,0 +1,10 @@
|
|||
import { IInjectorModule, IInjector } from "@paperbits/common/injection";
|
||||
import { DetailsOfApiHandlers } from "../detailsOfApiHandlers";
|
||||
import { DetailsOfApiEditor } from "./detailsOfApiEditor";
|
||||
|
||||
export class DetailsOfApiEditorModule implements IInjectorModule {
|
||||
public register(injector: IInjector): void {
|
||||
injector.bind("detailsOfApiEditor", DetailsOfApiEditor);
|
||||
injector.bindToCollection("widgetHandlers", DetailsOfApiHandlers, "detailsOfApiHandlers");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
import * as ko from "knockout";
|
||||
import template from "./detailsOfApiEditor.html";
|
||||
import { Component, OnMounted, Param, Event } from "@paperbits/common/ko/decorators";
|
||||
import { DetailsOfApiModel } from "../detailsOfApiModel";
|
||||
import { HyperlinkModel } from "@paperbits/common/permalinks";
|
||||
|
||||
@Component({
|
||||
selector: "details-of-api-editor",
|
||||
template: template
|
||||
})
|
||||
export class DetailsOfApiEditor {
|
||||
public readonly hyperlink: ko.Observable<HyperlinkModel>;
|
||||
public readonly hyperlinkTitle: ko.Computed<string>;
|
||||
|
||||
constructor() {
|
||||
this.hyperlink = ko.observable();
|
||||
this.hyperlinkTitle = ko.computed<string>(
|
||||
() => this.hyperlink() ? this.hyperlink().title : "Add a link...");
|
||||
}
|
||||
|
||||
@Param()
|
||||
public model: DetailsOfApiModel;
|
||||
|
||||
@Event()
|
||||
public onChange: (model: DetailsOfApiModel) => void;
|
||||
|
||||
@OnMounted()
|
||||
public async initialize(): Promise<void> {
|
||||
this.hyperlink(this.model.changeLogPageHyperlink);
|
||||
}
|
||||
|
||||
private applyChanges(): void {
|
||||
this.model.changeLogPageHyperlink = this.hyperlink();
|
||||
this.onChange(this.model);
|
||||
}
|
||||
|
||||
public onHyperlinkChange(hyperlink: HyperlinkModel): void {
|
||||
this.hyperlink(hyperlink);
|
||||
this.applyChanges();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
import * as ko from "knockout";
|
||||
import template from "./detailsOfApi.html";
|
||||
import { Component } from "@paperbits/common/ko/decorators";
|
||||
|
||||
@Component({
|
||||
selector: "detailsOfApi",
|
||||
template: template
|
||||
})
|
||||
export class DetailsOfApiViewModel {
|
||||
public readonly runtimeConfig: ko.Observable<string>;
|
||||
|
||||
constructor() {
|
||||
this.runtimeConfig = ko.observable();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
import { ViewModelBinder } from "@paperbits/common/widgets";
|
||||
import { DetailsOfApiViewModel } from "./detailsOfApiViewModel";
|
||||
import { DetailsOfApiModel } from "../detailsOfApiModel";
|
||||
import { Bag } from "@paperbits/common";
|
||||
import { EventManager } from "@paperbits/common/events";
|
||||
|
||||
|
||||
export class DetailsOfApiViewModelBinder implements ViewModelBinder<DetailsOfApiModel, DetailsOfApiViewModel> {
|
||||
|
||||
constructor(private readonly eventManager: EventManager) { }
|
||||
|
||||
public async modelToViewModel(model: DetailsOfApiModel, viewModel?: DetailsOfApiViewModel, bindingContext?: Bag<any>): Promise<DetailsOfApiViewModel> {
|
||||
if (!viewModel) {
|
||||
viewModel = new DetailsOfApiViewModel();
|
||||
}
|
||||
|
||||
viewModel.runtimeConfig(JSON.stringify({
|
||||
changeLogPageUrl: model.changeLogPageHyperlink
|
||||
? model.changeLogPageHyperlink.href
|
||||
: undefined
|
||||
}));
|
||||
|
||||
viewModel["widgetBinding"] = {
|
||||
displayName: "API: details",
|
||||
model: model,
|
||||
draggable: true,
|
||||
flow: "block",
|
||||
editor: "details-of-api-editor",
|
||||
applyChanges: async (updatedModel: DetailsOfApiModel) => {
|
||||
await this.modelToViewModel(updatedModel, viewModel, bindingContext);
|
||||
this.eventManager.dispatchEvent("onContentUpdate");
|
||||
}
|
||||
};
|
||||
|
||||
return viewModel;
|
||||
}
|
||||
|
||||
public canHandleModel(model: DetailsOfApiModel): boolean {
|
||||
return model instanceof DetailsOfApiModel;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
<div class="flex flex-row">
|
||||
<!-- ko if: working-->
|
||||
<spinner class="fit"></spinner>
|
||||
<!-- /ko -->
|
||||
|
||||
<!-- ko ifnot: working -->
|
||||
<div class="flex-item flex-grow animation-fade-in">
|
||||
<!-- ko if: api -->
|
||||
<h1>
|
||||
<span data-bind="text: api().displayName"></span>
|
||||
<!-- ko if: api().type === 'soap' -->
|
||||
<span class="badge badge-soap">SOAP</span>
|
||||
<!-- /ko -->
|
||||
</h1>
|
||||
|
||||
<div class="form-inline">
|
||||
<div class="form-group">
|
||||
<!-- ko if: versionApis().length > 0 -->
|
||||
<label for="apiVersions">API version: </label>
|
||||
<select id="apiVersions" class="form-control"
|
||||
data-bind="options: versionApis, optionsValue: 'name', optionsText: 'apiVersion', value: currentApiVersion">
|
||||
</select>
|
||||
<!-- /ko -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ko if: api().description -->
|
||||
<p data-bind="markdown: api().description"></p>
|
||||
<!-- /ko -->
|
||||
|
||||
<!-- /ko -->
|
||||
|
||||
<!-- ko ifnot: api -->
|
||||
<p>The specified API does not exist.</p>
|
||||
<!-- /ko -->
|
||||
|
||||
</div>
|
||||
<!-- /ko -->
|
||||
|
||||
|
||||
</div>
|
|
@ -0,0 +1,169 @@
|
|||
import * as ko from "knockout";
|
||||
import template from "./api-details.html";
|
||||
import { Component, OnMounted, RuntimeComponent, OnDestroyed, Param } from "@paperbits/common/ko/decorators";
|
||||
import { Router } from "@paperbits/common/routing";
|
||||
import { ApiService } from "../../../../../services/apiService";
|
||||
import { Api } from "../../../../../models/api";
|
||||
import { RouteHelper } from "../../../../../routing/routeHelper";
|
||||
|
||||
|
||||
@RuntimeComponent({
|
||||
selector: "api-details"
|
||||
})
|
||||
@Component({
|
||||
selector: "api-details",
|
||||
template: template
|
||||
})
|
||||
export class ApiDetails {
|
||||
public readonly api: ko.Observable<Api>;
|
||||
public readonly selectedApiName: ko.Observable<string>;
|
||||
public readonly currentApiVersion: ko.Observable<string>;
|
||||
public readonly versionApis: ko.ObservableArray<Api>;
|
||||
public readonly working: ko.Observable<boolean>;
|
||||
public readonly downloadSelected: ko.Observable<string>;
|
||||
|
||||
constructor(
|
||||
private readonly apiService: ApiService,
|
||||
private readonly routeHelper: RouteHelper,
|
||||
private readonly router: Router,
|
||||
) {
|
||||
this.changeLogPageUrl = ko.observable();
|
||||
this.api = ko.observable();
|
||||
this.selectedApiName = ko.observable();
|
||||
this.versionApis = ko.observableArray([]);
|
||||
this.working = ko.observable(false);
|
||||
this.currentApiVersion = ko.observable();
|
||||
this.downloadSelected = ko.observable("");
|
||||
this.loadApi = this.loadApi.bind(this);
|
||||
}
|
||||
|
||||
@Param()
|
||||
public changeLogPageUrl: ko.Observable<string>;
|
||||
|
||||
@OnMounted()
|
||||
public async initialize(): Promise<void> {
|
||||
const apiName = this.routeHelper.getApiName();
|
||||
|
||||
if (!apiName) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.selectedApiName(apiName);
|
||||
await this.loadApi(apiName);
|
||||
|
||||
this.router.addRouteChangeListener(this.onRouteChange);
|
||||
this.currentApiVersion.subscribe(this.onVersionChange);
|
||||
this.downloadSelected.subscribe(this.onDownloadChange);
|
||||
}
|
||||
|
||||
private async onRouteChange(): Promise<void> {
|
||||
const apiName = this.routeHelper.getApiName();
|
||||
|
||||
if (!apiName || apiName === this.selectedApiName()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.selectedApiName(apiName);
|
||||
await this.loadApi(apiName);
|
||||
}
|
||||
|
||||
public async loadApi(apiName: string): Promise<void> {
|
||||
if (!apiName) {
|
||||
this.api(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const api = await this.apiService.getApi(`apis/${apiName}`);
|
||||
if (!api) {
|
||||
this.api(null);
|
||||
return;
|
||||
}
|
||||
|
||||
this.working(true);
|
||||
|
||||
// if (api.apiVersionSet && api.apiVersionSet.id) {
|
||||
// const apis = await this.apiService.getApisInVersionSet(api.apiVersionSet.id);
|
||||
// apis.forEach(x => x.apiVersion = x.apiVersion || "Original");
|
||||
|
||||
// this.versionApis(apis || []);
|
||||
// }
|
||||
// else {
|
||||
this.versionApis([]);
|
||||
// }
|
||||
|
||||
this.currentApiVersion(api.name);
|
||||
this.api(api);
|
||||
|
||||
this.working(false);
|
||||
}
|
||||
|
||||
public async onDownloadChange(): Promise<void> {
|
||||
const definitionType = this.downloadSelected();
|
||||
|
||||
if (!definitionType) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.api() && this.api().id) {
|
||||
let exportObject = await this.apiService.exportApi(this.api().id, definitionType);
|
||||
let fileName = this.api().name;
|
||||
let fileType = "application/json";
|
||||
|
||||
switch (definitionType) {
|
||||
case "wsdl":
|
||||
case "wadl":
|
||||
fileType = "text/xml";
|
||||
fileName = `${fileName}.${definitionType}.xml`;
|
||||
break;
|
||||
case "openapi": // yaml 3.0
|
||||
fileName = `${fileName}.yaml`;
|
||||
break;
|
||||
default:
|
||||
fileName = `${fileName}.json`;
|
||||
exportObject = JSON.stringify(exportObject, null, 4);
|
||||
break;
|
||||
}
|
||||
this.download(exportObject, fileName, fileType);
|
||||
}
|
||||
|
||||
setTimeout(() => this.downloadSelected(""), 100);
|
||||
}
|
||||
|
||||
private download(data: string, filename: string, type: string): void {
|
||||
const file = new Blob([data], { type: type });
|
||||
|
||||
if (window.navigator.msSaveOrOpenBlob) { // IE10+
|
||||
window.navigator.msSaveOrOpenBlob(file, filename);
|
||||
}
|
||||
else { // Others
|
||||
const a = document.createElement("a"),
|
||||
url = URL.createObjectURL(file);
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(a);
|
||||
window.URL.revokeObjectURL(url);
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private onVersionChange(selectedApiName: string): void {
|
||||
const apiName = this.routeHelper.getApiName();
|
||||
if (apiName !== selectedApiName) {
|
||||
const apiUrl = this.routeHelper.getApiReferenceUrl(selectedApiName);
|
||||
this.router.navigateTo(apiUrl);
|
||||
}
|
||||
}
|
||||
|
||||
public getChangeLogUrl(): string {
|
||||
return this.routeHelper.getApiReferenceUrl(this.selectedApiName(), this.changeLogPageUrl());
|
||||
}
|
||||
|
||||
@OnDestroyed()
|
||||
public dispose(): void {
|
||||
this.router.removeRouteChangeListener(this.onRouteChange);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
<!-- ko if: layout() === 'list' -->
|
||||
<api-list data-bind="attr: { params: runtimeConfig }"></api-list>
|
||||
<!-- /ko -->
|
||||
|
||||
<!-- ko if: layout() === 'dropdown' -->
|
||||
<api-list-dropdown data-bind="attr: { params: runtimeConfig }"></api-list-dropdown>
|
||||
<!-- /ko -->
|
||||
|
||||
<!-- ko if: layout() === 'tiles' -->
|
||||
<api-list-tiles data-bind="attr: { params: runtimeConfig }"></api-list-tiles>
|
||||
<!-- /ko -->
|
|
@ -0,0 +1,11 @@
|
|||
import { IInjectorModule, IInjector } from "@paperbits/common/injection";
|
||||
import { ListOfApisModelBinder } from "../listOfApisModelBinder";
|
||||
import { ListOfApisViewModelBinder } from "./listOfApisViewModelBinder";
|
||||
|
||||
|
||||
export class ListOfApisModule implements IInjectorModule {
|
||||
public register(injector: IInjector): void {
|
||||
injector.bindToCollection("modelBinders", ListOfApisModelBinder);
|
||||
injector.bindToCollection("viewModelBinders", ListOfApisViewModelBinder);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
<fieldset class="form flex-item flex-item-grow" data-bind="scrollable: {}">
|
||||
<div class="form-group">
|
||||
<label for="allowSelection" class="form-label">
|
||||
<input type="checkbox" id="allowSelection" name="allowSelection" data-bind="checked: allowSelection" />
|
||||
Allow selection
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="defaultGroupByTagToEnabled" class="form-label">
|
||||
<input type="checkbox" id="defaultGroupByTagToEnabled" name="defaultGroupByTagToEnabled" data-bind="checked: defaultGroupByTagToEnabled" />
|
||||
Default Group by tag to enabled
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="label-group">
|
||||
<label class="form-label">
|
||||
Link to API details page
|
||||
</label>
|
||||
<button class="btn btn-info" type="button" title="Help" aria-label="Help"
|
||||
data-bind="tooltipToggle: 'A link to be navigated on click.'">
|
||||
</button>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<input id="hyperlink" class="form-control" readonly data-bind="value: hyperlinkTitle" />
|
||||
<button class="btn input-group-btn" aria-label="Select hyperlink"
|
||||
data-bind="balloon: { position: 'right', component: { name: 'hyperlink-selector', params: { hyperlink: $component.hyperlink, onChange: $component.onHyperlinkChange } } }">
|
||||
<i class="paperbits-icon paperbits-link-69-2"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
|
@ -0,0 +1,12 @@
|
|||
import { ListOfApisEditor } from "./listOfApisEditor";
|
||||
import { IInjectorModule, IInjector } from "@paperbits/common/injection";
|
||||
import { ListOfApisHandlers, ListOfApisTilesHandlers, ListOfApisDropdownHandlers } from "../listOfApisHandlers";
|
||||
|
||||
export class ListOfApisEditorModule implements IInjectorModule {
|
||||
public register(injector: IInjector): void {
|
||||
injector.bind("listOfApisEditor", ListOfApisEditor);
|
||||
injector.bindToCollection("widgetHandlers", ListOfApisHandlers, "listOfApisHandlers");
|
||||
injector.bindToCollection("widgetHandlers", ListOfApisTilesHandlers, "listOfApisTilesHandlers");
|
||||
injector.bindToCollection("widgetHandlers", ListOfApisDropdownHandlers, "listOfApisDropdownHandlers");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
import * as ko from "knockout";
|
||||
import template from "./listOfApisEditor.html";
|
||||
import { Component, OnMounted, Param, Event } from "@paperbits/common/ko/decorators";
|
||||
import { HyperlinkModel } from "@paperbits/common/permalinks";
|
||||
import { ListOfApisModel } from "../listOfApisModel";
|
||||
|
||||
|
||||
@Component({
|
||||
selector: "list-of-apis-editor",
|
||||
template: template
|
||||
})
|
||||
export class ListOfApisEditor {
|
||||
public readonly itemStyles: ko.ObservableArray<any>;
|
||||
public readonly itemStyle: ko.Observable<string>;
|
||||
public readonly allowSelection: ko.Observable<boolean>;
|
||||
public readonly defaultGroupByTagToEnabled: ko.Observable<boolean>;
|
||||
public readonly hyperlink: ko.Observable<HyperlinkModel>;
|
||||
public readonly hyperlinkTitle: ko.Computed<string>;
|
||||
|
||||
constructor() {
|
||||
this.allowSelection = ko.observable(false);
|
||||
this.defaultGroupByTagToEnabled = ko.observable(false);
|
||||
this.hyperlink = ko.observable();
|
||||
this.hyperlinkTitle = ko.computed<string>(
|
||||
() => this.hyperlink()
|
||||
? this.hyperlink().title
|
||||
: "Add a link...");
|
||||
|
||||
this.itemStyles = ko.observableArray<any>([
|
||||
{ name: "List", styleValue: "list" },
|
||||
{ name: "Tiles", styleValue: "tiles" },
|
||||
{ name: "Dropdown", styleValue: "dropdown" }
|
||||
]);
|
||||
|
||||
this.itemStyle = ko.observable<any>();
|
||||
}
|
||||
|
||||
@Param()
|
||||
public model: ListOfApisModel;
|
||||
|
||||
@Event()
|
||||
public onChange: (model: ListOfApisModel) => void;
|
||||
|
||||
@OnMounted()
|
||||
public async initialize(): Promise<void> {
|
||||
this.allowSelection(this.model.allowSelection);
|
||||
this.defaultGroupByTagToEnabled(this.model.defaultGroupByTagToEnabled);
|
||||
this.hyperlink(this.model.detailsPageHyperlink);
|
||||
|
||||
this.allowSelection.subscribe(this.applyChanges);
|
||||
this.defaultGroupByTagToEnabled.subscribe(this.applyChanges);
|
||||
}
|
||||
|
||||
private applyChanges(): void {
|
||||
this.model.allowSelection = this.allowSelection();
|
||||
this.model.defaultGroupByTagToEnabled = this.defaultGroupByTagToEnabled();
|
||||
this.model.detailsPageHyperlink = this.hyperlink();
|
||||
this.onChange(this.model);
|
||||
}
|
||||
|
||||
public onHyperlinkChange(hyperlink: HyperlinkModel): void {
|
||||
this.hyperlink(hyperlink);
|
||||
this.applyChanges();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
import * as ko from "knockout";
|
||||
import template from "./listOfApis.html";
|
||||
import { Component } from "@paperbits/common/ko/decorators";
|
||||
|
||||
@Component({
|
||||
selector: "listOfApis",
|
||||
template: template
|
||||
})
|
||||
export class ListOfApisViewModel {
|
||||
public readonly layout: ko.Observable<string>;
|
||||
public readonly runtimeConfig: ko.Observable<string>;
|
||||
|
||||
constructor() {
|
||||
this.layout = ko.observable();
|
||||
this.runtimeConfig = ko.observable();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
import { ViewModelBinder } from "@paperbits/common/widgets";
|
||||
import { ListOfApisViewModel } from "./listOfApisViewModel";
|
||||
import { ListOfApisModel } from "../listOfApisModel";
|
||||
import { Bag } from "@paperbits/common";
|
||||
import { EventManager } from "@paperbits/common/events";
|
||||
|
||||
|
||||
export class ListOfApisViewModelBinder implements ViewModelBinder<ListOfApisModel, ListOfApisViewModel> {
|
||||
|
||||
constructor(private readonly eventManager: EventManager) { }
|
||||
|
||||
public async modelToViewModel(model: ListOfApisModel, viewModel?: ListOfApisViewModel, bindingContext?: Bag<any>): Promise<ListOfApisViewModel> {
|
||||
if (!viewModel) {
|
||||
viewModel = new ListOfApisViewModel();
|
||||
}
|
||||
|
||||
viewModel.layout(model.layout);
|
||||
|
||||
viewModel.runtimeConfig(JSON.stringify({
|
||||
allowSelection: model.allowSelection,
|
||||
defaultGroupByTagToEnabled: model.defaultGroupByTagToEnabled,
|
||||
detailsPageUrl: model.detailsPageHyperlink
|
||||
? model.detailsPageHyperlink.href
|
||||
: undefined
|
||||
}));
|
||||
|
||||
viewModel["widgetBinding"] = {
|
||||
displayName: "List of APIs",
|
||||
model: model,
|
||||
draggable: true,
|
||||
flow: "block",
|
||||
editor: "list-of-apis-editor",
|
||||
applyChanges: async (updatedModel: ListOfApisModel) => {
|
||||
await this.modelToViewModel(updatedModel, viewModel, bindingContext);
|
||||
this.eventManager.dispatchEvent("onContentUpdate");
|
||||
}
|
||||
};
|
||||
|
||||
return viewModel;
|
||||
}
|
||||
|
||||
public canHandleModel(model: ListOfApisModel): boolean {
|
||||
return model instanceof ListOfApisModel;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
<div class="dropdown-container collapsible">
|
||||
<div class="form-control" tabindex="0" role="button" aria-label="APIs" data-toggle="collapsible">
|
||||
<div class="input-group">
|
||||
<div class="input">
|
||||
<span data-bind="text: selection"></span>
|
||||
<!-- ko if: selectedApi() && selectedApi().type === 'soap' -->
|
||||
<span class="badge badge-soap">SOAP</span>
|
||||
<!-- /ko -->
|
||||
</div>
|
||||
<div class="input-group-append">
|
||||
<i class="icon-emb icon-emb-chevron-down"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="collapsible-dropdown collapsible-content">
|
||||
<div class="search-input">
|
||||
<div class="input-group">
|
||||
<input type="search" role="searchbox" aria-label="Search" placeholder="Search APIs"
|
||||
data-bind="textInput: pattern" autofocus />
|
||||
<button type="button" class="search-button" aria-label="Search APIs">
|
||||
<i class="icon-emb icon-emb-magnifier"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <tag-input params="{ scope: 'apis', onChange: onTagsChange }"></tag-input> -->
|
||||
<div role="list">
|
||||
<!-- ko if: working -->
|
||||
<spinner role="presentation"></spinner>
|
||||
<!-- /ko -->
|
||||
|
||||
<!-- ko ifnot: working -->
|
||||
<!-- ko foreach: { data: apiGroups, as: 'group' } -->
|
||||
<div class="collapsible">
|
||||
<div class="tag-group">
|
||||
<span class="tag-item" role="group" data-bind="text: group.tag"></span>
|
||||
</div>
|
||||
<div class="collapsible-container">
|
||||
<div class="menu menu-vertical" role="list">
|
||||
<!-- ko foreach: { data: group.items, as: 'item' } -->
|
||||
<a href="#" role="listitem" class="nav-link text-truncate"
|
||||
data-bind="attr: { href: $component.getReferenceUrl(item) }, css: { 'nav-link-active': $component.selectedApiName() === item.name }">
|
||||
<span data-bind="text: item.displayName"></span>
|
||||
<!-- ko if: item.type === 'soap' -->
|
||||
<span class="badge badge-soap">SOAP</span>
|
||||
<!-- /ko -->
|
||||
<!-- ko if: item.apiVersion -->
|
||||
- <span data-bind="text: item.apiVersion"></span>
|
||||
<!-- /ko -->
|
||||
</a>
|
||||
<!-- /ko -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /ko -->
|
||||
|
||||
<!-- ko if: apiGroups().length === 0 -->
|
||||
<div class="list-item-empty">No APIs found</div>
|
||||
<!-- /ko -->
|
||||
|
||||
<!-- ko if: hasPager -->
|
||||
<ul class="pagination justify-content-center" role="navigation" aria-label="Pagination">
|
||||
<!-- ko if: hasPrevPage -->
|
||||
<li class="page-item">
|
||||
<a href="#" class="page-link" role="button" aria-label="Previous page"
|
||||
data-bind="click: prevPage, enable: hasPrevPage">
|
||||
<i class="icon-emb icon-emb-chevron-left"></i>
|
||||
</a>
|
||||
</li>
|
||||
<!-- /ko -->
|
||||
<li class="page-item">
|
||||
<span class="page-link" data-bind="text: page"></span>
|
||||
</li>
|
||||
<!-- ko if: hasNextPage -->
|
||||
<li class="page-item">
|
||||
<a href="#" class="page-link" role="button" aria-label="Next page"
|
||||
data-bind="click: nextPage, enable: hasNextPage">
|
||||
<i class="icon-emb icon-emb-chevron-right"></i>
|
||||
</a>
|
||||
</li>
|
||||
<!-- /ko -->
|
||||
</ul>
|
||||
<!-- /ko -->
|
||||
|
||||
<!-- /ko -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,173 @@
|
|||
import * as ko from "knockout";
|
||||
import * as Constants from "../../../../../constants";
|
||||
import template from "./api-list-dropdown.html";
|
||||
import { Component, RuntimeComponent, OnMounted, Param, OnDestroyed } from "@paperbits/common/ko/decorators";
|
||||
import { Router } from "@paperbits/common/routing";
|
||||
import { RouteHelper } from "../../../../../routing/routeHelper";
|
||||
import { Api } from "../../../../../models/api";
|
||||
import { ApiService } from "../../../../../services/apiService";
|
||||
import { TagGroup } from "../../../../../models/tagGroup";
|
||||
import { SearchQuery } from "../../../../../contracts/searchQuery";
|
||||
import { Utils } from "../../../../../utils";
|
||||
import { Tag } from "../../../../../models/tag";
|
||||
|
||||
|
||||
@RuntimeComponent({
|
||||
selector: "api-list-dropdown"
|
||||
})
|
||||
@Component({
|
||||
selector: "api-list-dropdown",
|
||||
template: template
|
||||
})
|
||||
export class ApiListDropdown {
|
||||
public readonly apiGroups: ko.ObservableArray<TagGroup<Api>>;
|
||||
public readonly selectedApi: ko.Observable<Api>;
|
||||
public readonly selectedApiName: ko.Observable<string>;
|
||||
public readonly working: ko.Observable<boolean>;
|
||||
public readonly pattern: ko.Observable<string>;
|
||||
public readonly tags: ko.Observable<Tag[]>;
|
||||
public readonly page: ko.Observable<number>;
|
||||
public readonly hasPager: ko.Computed<boolean>;
|
||||
public readonly hasPrevPage: ko.Observable<boolean>;
|
||||
public readonly hasNextPage: ko.Observable<boolean>;
|
||||
public readonly selection: ko.Computed<string>;
|
||||
|
||||
constructor(
|
||||
private readonly apiService: ApiService,
|
||||
private readonly router: Router,
|
||||
private readonly routeHelper: RouteHelper
|
||||
) {
|
||||
this.detailsPageUrl = ko.observable();
|
||||
this.allowSelection = ko.observable(false);
|
||||
this.working = ko.observable();
|
||||
this.selectedApi = ko.observable();
|
||||
this.selectedApiName = ko.observable();
|
||||
this.pattern = ko.observable();
|
||||
this.tags = ko.observable([]);
|
||||
this.page = ko.observable(1);
|
||||
this.hasPrevPage = ko.observable();
|
||||
this.hasNextPage = ko.observable();
|
||||
this.hasPager = ko.computed(() => this.hasPrevPage() || this.hasNextPage());
|
||||
this.apiGroups = ko.observableArray();
|
||||
this.selection = ko.computed(() => {
|
||||
const api = ko.unwrap(this.selectedApi);
|
||||
return api ? api.versionedDisplayName : "Select API";
|
||||
});
|
||||
}
|
||||
|
||||
@Param()
|
||||
public allowSelection: ko.Observable<boolean>;
|
||||
|
||||
@Param()
|
||||
public detailsPageUrl: ko.Observable<string>;
|
||||
|
||||
@OnMounted()
|
||||
public async initialize(): Promise<void> {
|
||||
await this.resetSearch();
|
||||
await this.checkSelection();
|
||||
|
||||
this.pattern
|
||||
.extend({ rateLimit: { timeout: Constants.defaultInputDelayMs, method: "notifyWhenChangesStop" } })
|
||||
.subscribe(this.resetSearch);
|
||||
|
||||
this.tags
|
||||
.subscribe(this.resetSearch);
|
||||
|
||||
this.router.addRouteChangeListener(this.onRouteChange);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates searching APIs.
|
||||
*/
|
||||
public async resetSearch(): Promise<void> {
|
||||
this.page(1);
|
||||
this.loadPageOfApis();
|
||||
}
|
||||
|
||||
private async onRouteChange(): Promise<void> {
|
||||
const apiName = this.routeHelper.getApiName();
|
||||
|
||||
if (apiName === this.selectedApiName()) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.checkSelection();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads page of APIs.
|
||||
*/
|
||||
public async loadPageOfApis(): Promise<void> {
|
||||
try {
|
||||
this.working(true);
|
||||
|
||||
const pageNumber = this.page() - 1;
|
||||
|
||||
const query: SearchQuery = {
|
||||
pattern: this.pattern(),
|
||||
tags: this.tags(),
|
||||
skip: pageNumber * Constants.defaultPageSize,
|
||||
take: Constants.defaultPageSize
|
||||
};
|
||||
|
||||
const pageOfTagResources = await this.apiService.getApisByTags(query);
|
||||
const apiGroups = pageOfTagResources.value;
|
||||
this.apiGroups(apiGroups);
|
||||
|
||||
const nextLink = pageOfTagResources.nextLink;
|
||||
|
||||
this.hasPrevPage(pageNumber > 0);
|
||||
this.hasNextPage(!!nextLink);
|
||||
}
|
||||
catch (error) {
|
||||
throw new Error(`Unable to load APIs. ${error.message}`);
|
||||
}
|
||||
finally {
|
||||
this.working(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async checkSelection(): Promise<void> {
|
||||
if (!this.allowSelection()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const apiName = this.routeHelper.getApiName();
|
||||
|
||||
if (!apiName) {
|
||||
return;
|
||||
}
|
||||
|
||||
const api = await this.apiService.getApi(`apis/${apiName}`);
|
||||
|
||||
if (!api) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.selectedApi(api);
|
||||
this.selectedApiName(apiName);
|
||||
}
|
||||
|
||||
public prevPage(): void {
|
||||
this.page(this.page() - 1);
|
||||
this.loadPageOfApis();
|
||||
}
|
||||
|
||||
public nextPage(): void {
|
||||
this.page(this.page() + 1);
|
||||
this.loadPageOfApis();
|
||||
}
|
||||
|
||||
public getReferenceUrl(api: Api): string {
|
||||
return this.routeHelper.getApiReferenceUrl(api.name, this.detailsPageUrl());
|
||||
}
|
||||
|
||||
public async onTagsChange(tags: Tag[]): Promise<void> {
|
||||
this.tags(tags);
|
||||
}
|
||||
|
||||
@OnDestroyed()
|
||||
public dispose(): void {
|
||||
this.router.removeRouteChangeListener(this.onRouteChange);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,115 @@
|
|||
<div class="form-inline search-input">
|
||||
<div class="d-block flex-grow">
|
||||
<div class="input-group">
|
||||
<input type="search" role="searchbox" aria-label="Search APIs" placeholder="Search APIs" spellcheck="false"
|
||||
data-bind="textInput: pattern" />
|
||||
<button type="button" class="search-button" aria-label="Search APIs">
|
||||
<i class="icon-emb icon-emb-magnifier"></i>
|
||||
</button>
|
||||
</div>
|
||||
<!-- <tag-input params="{ scope: 'apis', onChange: onTagsChange }"></tag-input> -->
|
||||
</div>
|
||||
<!-- <div class="form-group ml-auto">
|
||||
<label for="groupByTag">Group by tag
|
||||
<div class="switch">
|
||||
<input id="groupByTag" type="checkbox" data-bind="checked: $component.groupByTag">
|
||||
<span class="slider round"></span>
|
||||
</div>
|
||||
</label>
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
<div class="cards">
|
||||
<!-- ko if: working -->
|
||||
<div class="cards-body">
|
||||
<spinner class="fit"></spinner>
|
||||
</div>
|
||||
<!-- /ko -->
|
||||
|
||||
<!-- ko ifnot: working -->
|
||||
<div class="cards-body animation-fade-in">
|
||||
<!-- ko if: groupByTag -->
|
||||
<!-- ko foreach: { data: apiGroups, as: 'group' } -->
|
||||
<div class="tag-group">
|
||||
<span class="tag-item" role="group" data-bind="text: group.tag"></span>
|
||||
</div>
|
||||
|
||||
<!-- ko foreach: { data: group.items, as: 'item' } -->
|
||||
<a href="#" data-bind="attr: { href: $component.getReferenceUrl(item) }">
|
||||
<div class="card card-default">
|
||||
<img data-bind="attr: { src: item.thumbnail }" class="card-image" />
|
||||
<h3>
|
||||
<span data-bind="text: item.displayName"></span>
|
||||
<!-- ko if: item.apiVersion -->
|
||||
- <span data-bind="text: item.apiVersion"></span>
|
||||
<!-- /ko -->
|
||||
</h3>
|
||||
<p class="tile-content" data-bind="markdown: { source: item.description, truncateAt: 250 }"></p>
|
||||
</div>
|
||||
</a>
|
||||
<!-- /ko -->
|
||||
|
||||
<!-- /ko -->
|
||||
|
||||
<!-- ko if: apiGroups().length === 0 -->
|
||||
<p>No APIs found</p>
|
||||
<!-- /ko -->
|
||||
|
||||
<!-- /ko -->
|
||||
|
||||
<!-- ko ifnot: groupByTag -->
|
||||
|
||||
<!-- ko foreach: { data: apis, as: 'item' } -->
|
||||
<a href="#" data-bind="attr: { href: $component.getReferenceUrl(item) }">
|
||||
<div class="card card-default">
|
||||
<img data-bind="attr: { src: item.thumbnail }" class="card-thumbnail" />
|
||||
<div class="card-body">
|
||||
<h3>
|
||||
<span data-bind="text: item.displayName"></span>
|
||||
<!-- ko if: item.apiVersion -->
|
||||
- <span data-bind="text: item.apiVersion"></span>
|
||||
<!-- /ko -->
|
||||
</h3>
|
||||
<div class="tile line-clamp">
|
||||
<p class="tile-content" data-bind="markdown: { source: item.description, truncateAt: 250 }"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<!-- /ko -->
|
||||
|
||||
|
||||
<!-- ko if: apis().length === 0 -->
|
||||
<p>No APIs found</p>
|
||||
<!-- /ko -->
|
||||
|
||||
<!-- /ko -->
|
||||
</div>
|
||||
<!-- /ko -->
|
||||
|
||||
<div class="cards-footer">
|
||||
<!-- ko if: hasPager -->
|
||||
<ul class="pagination" role="navigation" aria-label="Pagination">
|
||||
<!-- ko if: hasPrevPage -->
|
||||
<li class="page-item">
|
||||
<a href="#" class="page-link" role="button" aria-label="Previous page"
|
||||
data-bind="click: prevPage, enable: hasPrevPage">
|
||||
<i class="icon-emb icon-emb-chevron-left"></i>
|
||||
</a>
|
||||
</li>
|
||||
<!-- /ko -->
|
||||
<li class="page-item">
|
||||
<span class="page-link" data-bind="text: page"></span>
|
||||
</li>
|
||||
<!-- ko if: hasNextPage -->
|
||||
<li class="page-item">
|
||||
<a href="#" class="page-link" role="button" aria-label="Next page"
|
||||
data-bind="click: nextPage, enable: hasNextPage">
|
||||
<i class="icon-emb icon-emb-chevron-right"></i>
|
||||
</a>
|
||||
</li>
|
||||
<!-- /ko -->
|
||||
</ul>
|
||||
<!-- /ko -->
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,150 @@
|
|||
import * as ko from "knockout";
|
||||
import * as Constants from "../../../../../constants";
|
||||
import template from "./api-list-tiles.html";
|
||||
import { Component, RuntimeComponent, OnMounted, Param } from "@paperbits/common/ko/decorators";
|
||||
import { ApiService } from "../../../../../services/apiService";
|
||||
import { Api } from "../../../../../models/api";
|
||||
import { TagGroup } from "../../../../../models/tagGroup";
|
||||
import { SearchQuery } from "../../../../../contracts/searchQuery";
|
||||
import { RouteHelper } from "../../../../../routing/routeHelper";
|
||||
import { Tag } from "../../../../../models/tag";
|
||||
import { Utils } from "../../../../../utils";
|
||||
|
||||
|
||||
@RuntimeComponent({
|
||||
selector: "api-list-tiles"
|
||||
})
|
||||
@Component({
|
||||
selector: "api-list-tiles",
|
||||
template: template
|
||||
})
|
||||
export class ApiListTiles {
|
||||
public readonly apis: ko.ObservableArray<Api>;
|
||||
public readonly apiGroups: ko.ObservableArray<TagGroup<Api>>;
|
||||
public readonly selectedApiName: ko.Observable<string>;
|
||||
public readonly working: ko.Observable<boolean>;
|
||||
public readonly pattern: ko.Observable<string>;
|
||||
public readonly tags: ko.Observable<Tag[]>;
|
||||
public readonly page: ko.Observable<number>;
|
||||
public readonly hasPager: ko.Computed<boolean>;
|
||||
public readonly hasPrevPage: ko.Observable<boolean>;
|
||||
public readonly hasNextPage: ko.Observable<boolean>;
|
||||
public readonly groupByTag: ko.Observable<boolean>;
|
||||
|
||||
constructor(
|
||||
private readonly apiService: ApiService,
|
||||
private readonly routeHelper: RouteHelper
|
||||
) {
|
||||
this.detailsPageUrl = ko.observable();
|
||||
this.allowSelection = ko.observable(false);
|
||||
this.apis = ko.observableArray([]);
|
||||
this.working = ko.observable();
|
||||
this.selectedApiName = ko.observable().extend(<any>{ acceptChange: this.allowSelection });
|
||||
this.pattern = ko.observable();
|
||||
this.tags = ko.observable([]);
|
||||
this.page = ko.observable(1);
|
||||
this.hasPrevPage = ko.observable();
|
||||
this.hasNextPage = ko.observable();
|
||||
this.hasPager = ko.computed(() => this.hasPrevPage() || this.hasNextPage());
|
||||
this.apiGroups = ko.observableArray();
|
||||
this.groupByTag = ko.observable(false);
|
||||
this.defaultGroupByTagToEnabled = ko.observable(false);
|
||||
}
|
||||
|
||||
@Param()
|
||||
public allowSelection: ko.Observable<boolean>;
|
||||
|
||||
@Param()
|
||||
public defaultGroupByTagToEnabled: ko.Observable<boolean>;
|
||||
|
||||
@Param()
|
||||
public detailsPageUrl: ko.Observable<string>;
|
||||
|
||||
@OnMounted()
|
||||
public async initialize(): Promise<void> {
|
||||
this.groupByTag(this.defaultGroupByTagToEnabled());
|
||||
|
||||
await this.resetSearch();
|
||||
|
||||
this.pattern
|
||||
.extend({ rateLimit: { timeout: Constants.defaultInputDelayMs, method: "notifyWhenChangesStop" } })
|
||||
.subscribe(this.resetSearch);
|
||||
|
||||
this.tags
|
||||
.subscribe(this.resetSearch);
|
||||
|
||||
this.groupByTag
|
||||
.subscribe(this.resetSearch);
|
||||
}
|
||||
|
||||
public prevPage(): void {
|
||||
this.page(this.page() - 1);
|
||||
this.loadPageOfApis();
|
||||
}
|
||||
|
||||
public nextPage(): void {
|
||||
this.page(this.page() + 1);
|
||||
this.loadPageOfApis();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates searching APIs.
|
||||
*/
|
||||
public async resetSearch(): Promise<void> {
|
||||
this.page(1);
|
||||
this.loadPageOfApis();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads page of APIs.
|
||||
*/
|
||||
public async loadPageOfApis(): Promise<void> {
|
||||
try {
|
||||
this.working(true);
|
||||
|
||||
const pageNumber = this.page() - 1;
|
||||
|
||||
const query: SearchQuery = {
|
||||
pattern: this.pattern(),
|
||||
tags: this.tags(),
|
||||
skip: pageNumber * Constants.defaultPageSize,
|
||||
take: Constants.defaultPageSize
|
||||
};
|
||||
|
||||
let nextLink;
|
||||
|
||||
if (this.groupByTag()) {
|
||||
const pageOfTagResources = await this.apiService.getApisByTags(query);
|
||||
const apiGroups = pageOfTagResources.value;
|
||||
|
||||
this.apiGroups(apiGroups);
|
||||
|
||||
nextLink = pageOfTagResources.nextLink;
|
||||
}
|
||||
else {
|
||||
const pageOfApis = await this.apiService.getApis(query);
|
||||
const apis = pageOfApis ? pageOfApis.value : [];
|
||||
this.apis(apis);
|
||||
|
||||
nextLink = pageOfApis.nextLink;
|
||||
}
|
||||
|
||||
this.hasPrevPage(pageNumber > 0);
|
||||
this.hasNextPage(!!nextLink);
|
||||
}
|
||||
catch (error) {
|
||||
throw new Error(`Unable to load APIs. Error: ${error.message}`);
|
||||
}
|
||||
finally {
|
||||
this.working(false);
|
||||
}
|
||||
}
|
||||
|
||||
public getReferenceUrl(api: Api): string {
|
||||
return this.routeHelper.getApiReferenceUrl(api.name, this.detailsPageUrl());
|
||||
}
|
||||
|
||||
public async onTagsChange(tags: Tag[]): Promise<void> {
|
||||
this.tags(tags);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,132 @@
|
|||
<div class="form-inline search-input">
|
||||
<div class="d-block flex-grow">
|
||||
<div class="input-group">
|
||||
<input type="search" role="searchbox" aria-label="Search APIs" placeholder="Search APIs" spellcheck="false" data-bind="textInput: pattern" />
|
||||
<button type="button" class="search-button" aria-label="Search APIs">
|
||||
<i class="icon-emb icon-emb-magnifier"></i>
|
||||
</button>
|
||||
</div>
|
||||
<!-- <tag-input params="{ scope: 'apis', onChange: onTagsChange }"></tag-input> -->
|
||||
</div>
|
||||
<!-- <div class="form-group ml-auto">
|
||||
<label for="groupByTag">Group by tag
|
||||
<div class="switch">
|
||||
<input id="groupByTag" type="checkbox" data-bind="checked: $component.groupByTag">
|
||||
<span class="slider round"></span>
|
||||
</div>
|
||||
</label>
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
<div class="table" role="table" aria-label="APIs">
|
||||
<div class="table-head" role="rowgroup">
|
||||
<div class="table-row" role="row">
|
||||
<div tabindex="0" class="col-5" role="columnheader">Name</div>
|
||||
<div tabindex="0" class="col-7" role="columnheader">Description</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ko if: working -->
|
||||
<div class="table-body">
|
||||
<spinner></spinner>
|
||||
</div>
|
||||
<!-- /ko -->
|
||||
|
||||
<!-- ko ifnot: working -->
|
||||
<div class="table-body animation-fade-in" role="presentation">
|
||||
<!-- ko if: groupByTag -->
|
||||
<!-- ko foreach: { data: apiGroups, as: 'group' } -->
|
||||
<div tabindex="0" class="tag-group" role="presentation">
|
||||
<span class="tag-item" role="rowgroup" data-bind="text: group.tag"></span>
|
||||
</div>
|
||||
<!-- ko foreach: { data: group.items, as: 'item' } -->
|
||||
<div class="table-row" role="row">
|
||||
<div class="col-5 text-truncate" role="cell">
|
||||
<a href="#"
|
||||
data-bind="attr: { href: $component.getReferenceUrl(item), title: item.displayName }">
|
||||
<span data-bind="text: item.displayName"></span>
|
||||
<!-- ko if: item.type === 'soap' -->
|
||||
<span class="badge badge-soap">SOAP</span>
|
||||
<!-- /ko -->
|
||||
<!-- ko if: item.apiVersion -->
|
||||
- <span data-bind="text: item.apiVersion"></span>
|
||||
<!-- /ko -->
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-7" role="cell">
|
||||
<div tabindex="0" data-bind="markdown: { source: item.description, truncateAt: 250 }"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /ko -->
|
||||
<!-- /ko -->
|
||||
|
||||
<!-- ko if: apiGroups().length === 0 -->
|
||||
<div class="table-row" role="row">
|
||||
<div class="col-12">
|
||||
No APIs found
|
||||
</div>
|
||||
</div>
|
||||
<!-- /ko -->
|
||||
<!-- /ko -->
|
||||
|
||||
<!-- ko ifnot: groupByTag -->
|
||||
<!-- ko foreach: { data: apis, as: 'item' } -->
|
||||
<div class="table-row" role="row">
|
||||
<div class="col-5 text-truncate" role="cell">
|
||||
<a href="#"
|
||||
data-bind="attr: { href: $component.getReferenceUrl(item), title: item.displayName }">
|
||||
<span data-bind="text: item.displayName"></span>
|
||||
<!-- ko if: item.type === 'soap' -->
|
||||
<span class="badge badge-soap">SOAP</span>
|
||||
<!-- /ko -->
|
||||
<!-- ko if: item.apiVersion -->
|
||||
- <span data-bind="text: item.apiVersion"></span>
|
||||
<!-- /ko -->
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-7" role="cell">
|
||||
<div tabindex="0" data-bind="markdown: { source: item.description, truncateAt: 250 }"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /ko -->
|
||||
|
||||
<!-- ko if: apis().length === 0 -->
|
||||
<div class="table-row" role="row">
|
||||
<div class="col-12">
|
||||
No APIs found
|
||||
</div>
|
||||
</div>
|
||||
<!-- /ko -->
|
||||
|
||||
<!-- /ko -->
|
||||
</div>
|
||||
<!-- /ko -->
|
||||
|
||||
<!-- ko if: hasPager -->
|
||||
<!-- ko ifnot: working -->
|
||||
<div class="table-footer" role="presentation">
|
||||
<ul class="pagination justify-content-center" role="navigation" aria-label="Pagination">
|
||||
<!-- ko if: hasPrevPage -->
|
||||
<li role="presentation">
|
||||
<a href="#" class="page-link" role="button" aria-label="Previous page"
|
||||
data-bind="click: prevPage, enable: hasPrevPage">
|
||||
<i class="icon-emb icon-emb-chevron-left"></i>
|
||||
</a>
|
||||
</li>
|
||||
<!-- /ko -->
|
||||
<li role="presentation">
|
||||
<span class="page-link" data-bind="text: page"></span>
|
||||
</li>
|
||||
<!-- ko if: hasNextPage -->
|
||||
<li role="presentation">
|
||||
<a href="#" class="page-link" role="button" aria-label="Next page"
|
||||
data-bind="click: nextPage, enable: hasNextPage">
|
||||
<i class="icon-emb icon-emb-chevron-right"></i>
|
||||
</a>
|
||||
</li>
|
||||
<!-- /ko -->
|
||||
</ul>
|
||||
</div>
|
||||
<!-- /ko -->
|
||||
<!-- /ko -->
|
||||
</div>
|
|
@ -0,0 +1,145 @@
|
|||
import * as ko from "knockout";
|
||||
import * as Constants from "../../../../../constants";
|
||||
import template from "./api-list.html";
|
||||
import { Component, RuntimeComponent, OnMounted, Param } from "@paperbits/common/ko/decorators";
|
||||
import { Api } from "../../../../../models/api";
|
||||
import { ApiService } from "../../../../../services/apiService";
|
||||
import { TagGroup } from "../../../../../models/tagGroup";
|
||||
import { SearchQuery } from "../../../../../contracts/searchQuery";
|
||||
import { RouteHelper } from "../../../../../routing/routeHelper";
|
||||
import { Tag } from "../../../../../models/tag";
|
||||
import { Utils } from "../../../../../utils";
|
||||
|
||||
|
||||
@RuntimeComponent({
|
||||
selector: "api-list"
|
||||
})
|
||||
@Component({
|
||||
selector: "api-list",
|
||||
template: template
|
||||
})
|
||||
export class ApiList {
|
||||
public readonly apis: ko.ObservableArray<Api>;
|
||||
public readonly apiGroups: ko.ObservableArray<TagGroup<Api>>;
|
||||
public readonly working: ko.Observable<boolean>;
|
||||
public readonly pattern: ko.Observable<string>;
|
||||
public readonly tags: ko.Observable<Tag[]>;
|
||||
public readonly page: ko.Observable<number>;
|
||||
public readonly hasPager: ko.Computed<boolean>;
|
||||
public readonly hasPrevPage: ko.Observable<boolean>;
|
||||
public readonly hasNextPage: ko.Observable<boolean>;
|
||||
public readonly groupByTag: ko.Observable<boolean>;
|
||||
|
||||
constructor(
|
||||
private readonly apiService: ApiService,
|
||||
private readonly routeHelper: RouteHelper
|
||||
) {
|
||||
this.detailsPageUrl = ko.observable();
|
||||
this.allowSelection = ko.observable(false);
|
||||
this.apis = ko.observableArray([]);
|
||||
this.working = ko.observable();
|
||||
this.pattern = ko.observable();
|
||||
this.tags = ko.observable([]);
|
||||
this.page = ko.observable(1);
|
||||
this.hasPrevPage = ko.observable();
|
||||
this.hasNextPage = ko.observable();
|
||||
this.hasPager = ko.computed(() => this.hasPrevPage() || this.hasNextPage());
|
||||
this.apiGroups = ko.observableArray();
|
||||
this.groupByTag = ko.observable(false);
|
||||
this.defaultGroupByTagToEnabled = ko.observable(false);
|
||||
}
|
||||
|
||||
@Param()
|
||||
public allowSelection: ko.Observable<boolean>;
|
||||
|
||||
@Param()
|
||||
public defaultGroupByTagToEnabled: ko.Observable<boolean>;
|
||||
|
||||
@Param()
|
||||
public detailsPageUrl: ko.Observable<string>;
|
||||
|
||||
@OnMounted()
|
||||
public async initialize(): Promise<void> {
|
||||
this.groupByTag(this.defaultGroupByTagToEnabled());
|
||||
|
||||
await this.resetSearch();
|
||||
|
||||
this.pattern
|
||||
.extend({ rateLimit: { timeout: Constants.defaultInputDelayMs, method: "notifyWhenChangesStop" } })
|
||||
.subscribe(this.resetSearch);
|
||||
|
||||
this.tags
|
||||
.subscribe(this.resetSearch);
|
||||
|
||||
this.groupByTag
|
||||
.subscribe(this.resetSearch);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads page of APIs.
|
||||
*/
|
||||
public async loadPageOfApis(): Promise<void> {
|
||||
const pageNumber = this.page() - 1;
|
||||
|
||||
const query: SearchQuery = {
|
||||
pattern: this.pattern(),
|
||||
tags: this.tags(),
|
||||
skip: pageNumber * Constants.defaultPageSize,
|
||||
take: Constants.defaultPageSize
|
||||
};
|
||||
|
||||
let nextLink;
|
||||
|
||||
try {
|
||||
this.working(true);
|
||||
|
||||
if (this.groupByTag()) {
|
||||
const pageOfTagResources = await this.apiService.getApisByTags(query);
|
||||
const apiGroups = pageOfTagResources.value;
|
||||
|
||||
this.apiGroups(apiGroups);
|
||||
|
||||
nextLink = pageOfTagResources.nextLink;
|
||||
}
|
||||
else {
|
||||
const pageOfApis = await this.apiService.getApis(query);
|
||||
const apis = pageOfApis ? pageOfApis.value : [];
|
||||
this.apis(apis);
|
||||
|
||||
nextLink = pageOfApis.nextLink;
|
||||
}
|
||||
|
||||
this.hasPrevPage(pageNumber > 0);
|
||||
this.hasNextPage(!!nextLink);
|
||||
}
|
||||
catch (error) {
|
||||
throw new Error(`Unable to load APIs. Error: ${error.message}`);
|
||||
}
|
||||
finally {
|
||||
this.working(false);
|
||||
}
|
||||
}
|
||||
|
||||
public getReferenceUrl(api: Api): string {
|
||||
return this.routeHelper.getApiReferenceUrl(api.name, this.detailsPageUrl());
|
||||
}
|
||||
|
||||
public prevPage(): void {
|
||||
this.page(this.page() - 1);
|
||||
this.loadPageOfApis();
|
||||
}
|
||||
|
||||
public nextPage(): void {
|
||||
this.page(this.page() + 1);
|
||||
this.loadPageOfApis();
|
||||
}
|
||||
|
||||
public async resetSearch(): Promise<void> {
|
||||
this.page(1);
|
||||
this.loadPageOfApis();
|
||||
}
|
||||
|
||||
public async onTagsChange(tags: Tag[]): Promise<void> {
|
||||
this.tags(tags);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export * from "./api-list";
|
||||
export * from "./api-list-dropdown";
|
||||
export * from "./api-list-tiles";
|
|
@ -0,0 +1,5 @@
|
|||
import * as ko from "knockout";
|
||||
|
||||
export class TagGroupViewModel {
|
||||
public expanded: ko.Observable<boolean>;
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
import * as ko from "knockout";
|
||||
|
||||
export class TreeViewNode {
|
||||
public id: string;
|
||||
public name: string;
|
||||
public label: ko.Observable<string>;
|
||||
public expanded?: ko.Observable<boolean>;
|
||||
public nodes: ko.ObservableArray<TreeViewNode>;
|
||||
public data: ko.Observable<any>;
|
||||
public level?: ko.Observable<string>;
|
||||
public hasChildren: ko.Computed<boolean>;
|
||||
public hasActiveChild: ko.Computed<boolean>;
|
||||
|
||||
public onSelect: (node: TreeViewNode) => void;
|
||||
public isSelected: () => boolean;
|
||||
|
||||
constructor(label: string) {
|
||||
this.label = ko.observable(label);
|
||||
this.nodes = ko.observableArray([]);
|
||||
this.isSelected = ko.observable(false);
|
||||
this.expanded = ko.observable(false);
|
||||
this.level = ko.observable("level-1");
|
||||
this.data = ko.observable();
|
||||
|
||||
this.hasChildren = ko.pureComputed(() => {
|
||||
return this.nodes().length > 0;
|
||||
});
|
||||
|
||||
this.hasActiveChild = ko.pureComputed(() => {
|
||||
return this.hasChildren() && (this.nodes().some(x => x.hasActiveChild() || x.isSelected()));
|
||||
});
|
||||
}
|
||||
|
||||
public select(): void {
|
||||
this.expanded(true);
|
||||
this.onSelect(this);
|
||||
}
|
||||
|
||||
public toggle(): void {
|
||||
if (!this.expanded()) {
|
||||
this.expanded(true);
|
||||
this.select();
|
||||
}
|
||||
else {
|
||||
this.expanded(false);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
import { Contract } from "@paperbits/common";
|
||||
import { HyperlinkContract } from "@paperbits/common/editing";
|
||||
|
||||
|
||||
/**
|
||||
* API list widget contract.
|
||||
*/
|
||||
export interface ListOfApisContract extends Contract {
|
||||
/**
|
||||
* API list layout.
|
||||
*/
|
||||
itemStyleView?: string;
|
||||
|
||||
/**
|
||||
* Indicated that an APIs can be selected.
|
||||
*/
|
||||
allowSelection: boolean;
|
||||
|
||||
/**
|
||||
* Default GroupByTag to enabled.
|
||||
*/
|
||||
defaultGroupByTagToEnabled?: boolean;
|
||||
|
||||
/**
|
||||
* Link to a page that contains API details.
|
||||
*/
|
||||
detailsPageHyperlink?: HyperlinkContract;
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
import { IWidgetOrder, IWidgetHandler } from "@paperbits/common/editing";
|
||||
import { ListOfApisModel } from "./listOfApisModel";
|
||||
|
||||
export class ListOfApisHandlers implements IWidgetHandler {
|
||||
public async getWidgetOrder(): Promise<IWidgetOrder> {
|
||||
const widgetOrder: IWidgetOrder = {
|
||||
name: "listOfApis",
|
||||
category: "APIs",
|
||||
displayName: "List of APIs",
|
||||
iconClass: "paperbits-cheque-3",
|
||||
requires: ["html"],
|
||||
createModel: async () => new ListOfApisModel("list")
|
||||
};
|
||||
|
||||
return widgetOrder;
|
||||
}
|
||||
}
|
||||
export class ListOfApisTilesHandlers implements IWidgetHandler {
|
||||
public async getWidgetOrder(): Promise<IWidgetOrder> {
|
||||
const widgetOrder: IWidgetOrder = {
|
||||
name: "listOfApisTiles",
|
||||
category: "APIs",
|
||||
displayName: "List of APIs (tiles)",
|
||||
iconClass: "paperbits-cheque-3",
|
||||
requires: ["html"],
|
||||
createModel: async () => new ListOfApisModel("tiles")
|
||||
};
|
||||
|
||||
return widgetOrder;
|
||||
}
|
||||
}
|
||||
export class ListOfApisDropdownHandlers implements IWidgetHandler {
|
||||
public async getWidgetOrder(): Promise<IWidgetOrder> {
|
||||
const widgetOrder: IWidgetOrder = {
|
||||
name: "listOfApisDropdown",
|
||||
category: "APIs",
|
||||
displayName: "List of APIs (dropdown)",
|
||||
iconClass: "paperbits-cheque-3",
|
||||
requires: ["html"],
|
||||
createModel: async () => new ListOfApisModel("dropdown")
|
||||
};
|
||||
|
||||
return widgetOrder;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
import { HyperlinkModel } from "@paperbits/common/permalinks";
|
||||
|
||||
export class ListOfApisModel {
|
||||
/**
|
||||
* List layout.
|
||||
*/
|
||||
public layout?: string;
|
||||
|
||||
/**
|
||||
* Indicated that an operations can be selected.
|
||||
*/
|
||||
public allowSelection: boolean;
|
||||
|
||||
/**
|
||||
* Default GroupByTag to enabled.
|
||||
*/
|
||||
public defaultGroupByTagToEnabled: boolean;
|
||||
|
||||
/**
|
||||
* Link to a page that contains operation details.
|
||||
*/
|
||||
public detailsPageHyperlink: HyperlinkModel;
|
||||
|
||||
constructor(layout?: string) {
|
||||
this.layout = layout;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
import { Contract } from "@paperbits/common";
|
||||
import { IModelBinder } from "@paperbits/common/editing";
|
||||
import { ListOfApisModel } from "./listOfApisModel";
|
||||
import { ListOfApisContract } from "./listOfApisContract";
|
||||
import { IPermalinkResolver } from "@paperbits/common/permalinks";
|
||||
|
||||
|
||||
export class ListOfApisModelBinder implements IModelBinder<ListOfApisModel> {
|
||||
constructor(private readonly permalinkResolver: IPermalinkResolver) { }
|
||||
|
||||
public canHandleModel(model: Object): boolean {
|
||||
return model instanceof ListOfApisModel;
|
||||
}
|
||||
|
||||
public async contractToModel(contract: ListOfApisContract): Promise<ListOfApisModel> {
|
||||
const model = new ListOfApisModel();
|
||||
|
||||
model.layout = contract.itemStyleView;
|
||||
model.allowSelection = contract.allowSelection;
|
||||
model.defaultGroupByTagToEnabled = contract.defaultGroupByTagToEnabled === true;
|
||||
|
||||
if (contract.detailsPageHyperlink) {
|
||||
model.detailsPageHyperlink = await this.permalinkResolver.getHyperlinkFromContract(contract.detailsPageHyperlink);
|
||||
}
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
public canHandleContract(contract: Contract): boolean {
|
||||
return contract.type === "listOfApis";
|
||||
}
|
||||
|
||||
public modelToContract(model: ListOfApisModel): Contract {
|
||||
const contract: ListOfApisContract = {
|
||||
type: "listOfApis",
|
||||
itemStyleView: model.layout,
|
||||
allowSelection: model.allowSelection,
|
||||
defaultGroupByTagToEnabled: model.defaultGroupByTagToEnabled,
|
||||
detailsPageHyperlink: model.detailsPageHyperlink
|
||||
? {
|
||||
target: model.detailsPageHyperlink.target,
|
||||
targetKey: model.detailsPageHyperlink.targetKey
|
||||
}
|
||||
: null
|
||||
};
|
||||
|
||||
return contract;
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
<view-manager></view-manager>
|
|
@ -0,0 +1,79 @@
|
|||
import { AccessToken } from "./../../authentication/accessToken";
|
||||
import template from "./app.html";
|
||||
import { ViewManager } from "@paperbits/common/ui";
|
||||
import { Component, OnMounted } from "@paperbits/common/ko/decorators";
|
||||
import { ISettingsProvider } from "@paperbits/common/configuration";
|
||||
import { ISiteService } from "@paperbits/common/sites";
|
||||
import { IAuthenticator } from "../../authentication";
|
||||
import { Utils } from "../../utils";
|
||||
|
||||
const startupError = `Unable to start the portal`;
|
||||
|
||||
@Component({
|
||||
selector: "app",
|
||||
template: template
|
||||
})
|
||||
export class App {
|
||||
constructor(
|
||||
private readonly settingsProvider: ISettingsProvider,
|
||||
private readonly authenticator: IAuthenticator,
|
||||
private readonly viewManager: ViewManager,
|
||||
private readonly siteService: ISiteService
|
||||
) { }
|
||||
|
||||
@OnMounted()
|
||||
public async initialize(): Promise<void> {
|
||||
// const settings = await this.settingsProvider.getSettings();
|
||||
|
||||
// if (!settings["managementApiUrl"]) {
|
||||
// this.viewManager.addToast(startupError, `Management API URL is missing. See setting <i>managementApiUrl</i> in the configuration file <i>config.design.json</i>`);
|
||||
// return;
|
||||
// }
|
||||
|
||||
// try {
|
||||
// const token = await this.authenticator.getAccessToken();
|
||||
|
||||
// if (!token) {
|
||||
// const managementApiAccessToken = settings["managementApiAccessToken"];
|
||||
|
||||
// if (!managementApiAccessToken) {
|
||||
// this.viewManager.addToast(startupError, `Management API access token is missing. See setting <i>managementApiAccessToken</i> in the configuration file <i>config.design.json</i>`);
|
||||
// return;
|
||||
// }
|
||||
|
||||
// const accessToken = AccessToken.parse(managementApiAccessToken);
|
||||
// const utcNow = Utils.getUtcDateTime();
|
||||
|
||||
// if (utcNow >= accessToken.expires) {
|
||||
// this.viewManager.addToast(startupError, `Management API access token has expired. See setting <i>managementApiAccessToken</i> in the configuration file <i>config.design.json</i>`);
|
||||
// this.authenticator.clearAccessToken();
|
||||
// window.location.assign("/signout");
|
||||
// return;
|
||||
// }
|
||||
|
||||
// await this.authenticator.setAccessToken(accessToken);
|
||||
// }
|
||||
// }
|
||||
// catch (error) {
|
||||
// this.viewManager.addToast(startupError, error);
|
||||
// return;
|
||||
// }
|
||||
|
||||
try {
|
||||
/* Checking if settings were created, and if not, we consider the portal not initialized and launch setup dialog. */
|
||||
|
||||
// const siteSettings = await this.siteService.getSettings<any>();
|
||||
|
||||
// if (!siteSettings) {
|
||||
// this.viewManager.setHost({ name: "setup-dialog" });
|
||||
// return;
|
||||
// }
|
||||
|
||||
this.viewManager.setHost({ name: "page-host" });
|
||||
this.viewManager.showToolboxes();
|
||||
}
|
||||
catch (error) {
|
||||
this.viewManager.addToast(startupError, `Check if the settings specified in the configuration file <i>config.design.json</i> are correct or refer to the <a href="http://aka.ms/apimdocs/portal#faq" target="_blank">frequently asked questions</a>.`);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
import { Utils } from "../utils";
|
||||
import { IAuthenticator, AccessToken } from "./../authentication";
|
||||
import { HttpHeader } from "@paperbits/common/http/httpHeader";
|
||||
|
||||
export class DefaultAuthenticator implements IAuthenticator {
|
||||
public async getAccessToken(): Promise<string> {
|
||||
const accessToken = sessionStorage.getItem("accessToken");
|
||||
|
||||
if (!accessToken && window.location.pathname.startsWith("/signin-sso")) {
|
||||
const url = new URL(location.href);
|
||||
const queryParams = new URLSearchParams(url.search);
|
||||
const tokenValue = queryParams.get("token");
|
||||
const token = AccessToken.parse(`SharedAccessSignature ${tokenValue}`);
|
||||
await this.setAccessToken(token);
|
||||
|
||||
const returnUrl = queryParams.get("returnUrl") || "/";
|
||||
window.location.assign(returnUrl);
|
||||
}
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
public async setAccessToken(accessToken: AccessToken): Promise<void> {
|
||||
if (accessToken.isExpired()) {
|
||||
console.warn(`Cannot set expired access token.`);
|
||||
return;
|
||||
}
|
||||
|
||||
sessionStorage.setItem("accessToken", accessToken.toString());
|
||||
}
|
||||
|
||||
public async refreshAccessTokenFromHeader(responseHeaders: HttpHeader[] = []): Promise<string> {
|
||||
const accessTokenHeader = responseHeaders.find(x => x.name.toLowerCase() === "ocp-apim-sas-token");
|
||||
|
||||
if (accessTokenHeader?.value) {
|
||||
const accessToken = AccessToken.parse(accessTokenHeader.value);
|
||||
const accessTokenString = accessToken.toString();
|
||||
|
||||
const current = sessionStorage.getItem("accessToken");
|
||||
|
||||
if (current !== accessTokenString) {
|
||||
sessionStorage.setItem("accessToken", accessTokenString);
|
||||
return accessTokenString;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public clearAccessToken(): void {
|
||||
sessionStorage.removeItem("accessToken");
|
||||
}
|
||||
|
||||
public async isAuthenticated(): Promise<boolean> {
|
||||
const accessToken = await this.getAccessToken();
|
||||
|
||||
if (!accessToken) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const parsedToken = AccessToken.parse(accessToken);
|
||||
|
||||
if (!parsedToken) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !parsedToken.isExpired();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
<div tabindex="0" class="input-group" data-bind="activate: onClick">
|
||||
<input class="input" type="text" placeholder="Upload file" disabled data-bind="value: selectedFileInfo">
|
||||
<div class="input-group-append">
|
||||
<i class="icon-emb icon-emb-upload"></i>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,49 @@
|
|||
import * as ko from "knockout";
|
||||
import template from "./file-input.html";
|
||||
import { Utils } from "../../utils";
|
||||
import { Component, Event } from "@paperbits/common/ko/decorators";
|
||||
|
||||
|
||||
@Component({
|
||||
selector: "file-input",
|
||||
template: template
|
||||
})
|
||||
export class FileInput {
|
||||
private readonly input: HTMLInputElement;
|
||||
public selectedFileInfo: ko.Observable<string>;
|
||||
|
||||
@Event()
|
||||
public onSelect: (file: File) => void;
|
||||
|
||||
constructor() {
|
||||
this.selectedFileInfo = ko.observable<string>();
|
||||
this.input = document.createElement("input");
|
||||
this.input.type = "file";
|
||||
this.input.onchange = this.onChange.bind(this);
|
||||
}
|
||||
|
||||
public onKeyDown(event: KeyboardEvent): void {
|
||||
if (event.keyCode === 13) {
|
||||
event.preventDefault();
|
||||
this.input.click();
|
||||
}
|
||||
}
|
||||
|
||||
public onClick(): void {
|
||||
this.input.value = "";
|
||||
this.input.click();
|
||||
}
|
||||
|
||||
public onChange(event: any): void {
|
||||
if (event.target.files.length > 0) {
|
||||
const file: File = event.target.files[0];
|
||||
|
||||
this.selectedFileInfo(`${file.name} (${Utils.formatBytes(file.size)})`);
|
||||
|
||||
this.onSelect(file);
|
||||
}
|
||||
else {
|
||||
this.onSelect(null);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { IBlobStorage } from "@paperbits/common/persistence";
|
||||
|
||||
export class FileSystemBlobStorage implements IBlobStorage {
|
||||
private basePath: string;
|
||||
|
||||
constructor(basePath: string) {
|
||||
this.basePath = basePath;
|
||||
}
|
||||
|
||||
public async uploadBlob(blobPath: string, content: Uint8Array): Promise<void> {
|
||||
const fullpath = `${this.basePath}/${blobPath}`.replace("//", "/");
|
||||
|
||||
try {
|
||||
await fs.promises.mkdir(path.dirname(fullpath), { recursive: true });
|
||||
await fs.promises.writeFile(fullpath, Buffer.from(content.buffer));
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
public downloadBlob(blobPath: string): Promise<Uint8Array> {
|
||||
return new Promise<Uint8Array>((resolve, reject) => {
|
||||
const fullpath = `${this.basePath}/${blobPath}`.replace("//", "/");
|
||||
|
||||
fs.readFile(fullpath, (error, buffer: Buffer) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
|
||||
const arrayBuffer = new ArrayBuffer(buffer.length);
|
||||
const unit8Array = new Uint8Array(arrayBuffer);
|
||||
|
||||
for (let i = 0; i < buffer.length; ++i) {
|
||||
unit8Array[i] = buffer[i];
|
||||
}
|
||||
|
||||
resolve(unit8Array);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public async listBlobs(): Promise<string[]> {
|
||||
const files = this.listAllFilesInDirectory(this.basePath);
|
||||
if (files.length > 0) {
|
||||
return files.map(file => file.split(this.basePath).pop());
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
private listAllFilesInDirectory(dir: string): string[] {
|
||||
const results = [];
|
||||
|
||||
fs.readdirSync(dir).forEach((file) => {
|
||||
file = dir + "/" + file;
|
||||
const stat = fs.statSync(file);
|
||||
|
||||
if (stat && stat.isDirectory()) {
|
||||
results.push(...this.listAllFilesInDirectory(file));
|
||||
} else {
|
||||
results.push(file);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
public getDownloadUrl(filename: string): Promise<string> {
|
||||
throw new Error("Not supported");
|
||||
}
|
||||
|
||||
public async deleteBlob(filename: string): Promise<void> {
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
<operation-details data-bind="attr: { params: config }"></operation-details>
|
|
@ -0,0 +1,24 @@
|
|||
<fieldset class="form flex-item flex-item-grow" data-bind="scrollable: {}">
|
||||
<div class="form-group">
|
||||
<label for="enableConsole" class="form-label">
|
||||
<input type="checkbox" id="enableConsole" name="enableConsole" data-bind="checked: enableConsole" />
|
||||
Enable API console
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="enableScrollTo" class="form-label">
|
||||
<input type="checkbox" id="enableScrollTo" name="enableScrollTo" data-bind="checked: enableScrollTo" />
|
||||
Automatically scroll to operation name
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="defaultSchemaView" class="form-label">
|
||||
Default schema view
|
||||
</label>
|
||||
<select class="form-control" data-bind="value: defaultSchemaView">
|
||||
<option value="table">Table</value>
|
||||
<option value="raw">Raw schema</value>
|
||||
</select>
|
||||
</div>
|
||||
</fieldset>
|
|
@ -0,0 +1,43 @@
|
|||
import * as ko from "knockout";
|
||||
import template from "./operationDetailsEditor.html";
|
||||
import { Component, OnMounted, Param, Event } from "@paperbits/common/ko/decorators";
|
||||
import { OperationDetailsModel } from "../operationDetailsModel";
|
||||
|
||||
@Component({
|
||||
selector: "operation-details-editor",
|
||||
template: template
|
||||
})
|
||||
export class OperationDetailsEditor {
|
||||
public readonly enableConsole: ko.Observable<boolean>;
|
||||
public readonly enableScrollTo: ko.Observable<boolean>;
|
||||
public readonly defaultSchemaView: ko.Observable<string>;
|
||||
|
||||
constructor() {
|
||||
this.enableConsole = ko.observable();
|
||||
this.enableScrollTo = ko.observable();
|
||||
this.defaultSchemaView = ko.observable();
|
||||
}
|
||||
|
||||
@Param()
|
||||
public model: OperationDetailsModel;
|
||||
|
||||
@Event()
|
||||
public onChange: (model: OperationDetailsModel) => void;
|
||||
|
||||
@OnMounted()
|
||||
public async initialize(): Promise<void> {
|
||||
this.enableConsole(this.model.enableConsole);
|
||||
this.enableScrollTo(this.model.enableScrollTo);
|
||||
this.defaultSchemaView(this.model.defaultSchemaView || "table");
|
||||
this.enableConsole.subscribe(this.applyChanges);
|
||||
this.enableScrollTo.subscribe(this.applyChanges);
|
||||
this.defaultSchemaView.subscribe(this.applyChanges);
|
||||
}
|
||||
|
||||
private applyChanges(): void {
|
||||
this.model.enableConsole = this.enableConsole();
|
||||
this.model.enableScrollTo = this.enableScrollTo();
|
||||
this.model.defaultSchemaView = this.defaultSchemaView();
|
||||
this.onChange(this.model);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
import * as ko from "knockout";
|
||||
import template from "./operationDetails.html";
|
||||
import { Component } from "@paperbits/common/ko/decorators";
|
||||
|
||||
@Component({
|
||||
selector: "operationDetails",
|
||||
template: template
|
||||
})
|
||||
export class OperationDetailsViewModel {
|
||||
public readonly config?: ko.Observable<string>;
|
||||
|
||||
constructor() {
|
||||
this.config = ko.observable();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
import { ViewModelBinder } from "@paperbits/common/widgets";
|
||||
import { EventManager } from "@paperbits/common/events";
|
||||
import { OperationDetailsViewModel } from "./operationDetailsViewModel";
|
||||
import { OperationDetailsModel } from "../operationDetailsModel";
|
||||
import { Bag } from "@paperbits/common";
|
||||
|
||||
|
||||
export class OperationDetailsViewModelBinder implements ViewModelBinder<OperationDetailsModel, OperationDetailsViewModel> {
|
||||
constructor(private readonly eventManager: EventManager) { }
|
||||
|
||||
public async modelToViewModel(model: OperationDetailsModel, viewModel?: OperationDetailsViewModel, bindingContext?: Bag<any>): Promise<OperationDetailsViewModel> {
|
||||
if (!viewModel) {
|
||||
viewModel = new OperationDetailsViewModel();
|
||||
|
||||
viewModel["widgetBinding"] = {
|
||||
displayName: "Operation: details",
|
||||
model: model,
|
||||
draggable: true,
|
||||
flow: "block",
|
||||
editor: "operation-details-editor",
|
||||
applyChanges: async (updatedModel: OperationDetailsModel) => {
|
||||
await this.modelToViewModel(updatedModel, viewModel, bindingContext);
|
||||
this.eventManager.dispatchEvent("onContentUpdate");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const runtimeConfig = {
|
||||
enableConsole: model.enableConsole,
|
||||
enableScrollTo: model.enableScrollTo,
|
||||
authorizationServers: model.authorizationServers,
|
||||
defaultSchemaView: model.defaultSchemaView
|
||||
};
|
||||
|
||||
viewModel.config(JSON.stringify(runtimeConfig));
|
||||
|
||||
return viewModel;
|
||||
}
|
||||
|
||||
public canHandleModel(model: OperationDetailsModel): boolean {
|
||||
return model instanceof OperationDetailsModel;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
<div class="flex code-block code-block-bordered">
|
||||
<div class="code-block-heading">
|
||||
<span data-bind="text: language"></span>
|
||||
<button class="code-block-command" data-bind="copyToClipboard: sample"
|
||||
aria-label="Copy to clipboard">
|
||||
<i class="icon-emb icon-emb-duplicate"></i>
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<pre data-bind="syntaxHighlight: { code: content, language: language }"></pre>
|
|
@ -0,0 +1,21 @@
|
|||
import * as ko from "knockout";
|
||||
import template from "./code-sample.html";
|
||||
import { Component, Param } from "@paperbits/common/ko/decorators";
|
||||
|
||||
|
||||
@Component({
|
||||
selector: "code-sample",
|
||||
template: template
|
||||
})
|
||||
export class CodeSampleViewModel {
|
||||
constructor() {
|
||||
this.content = ko.observable();
|
||||
this.language = ko.observable();
|
||||
}
|
||||
|
||||
@Param()
|
||||
public readonly content: ko.Observable<string>;
|
||||
|
||||
@Param()
|
||||
public readonly language: ko.Observable<string>;
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
export interface StoredCredentials {
|
||||
grantType: string;
|
||||
accessToken: string;
|
||||
}
|
||||
|
||||
export interface OAuthSession {
|
||||
[apiName: string]: StoredCredentials;
|
||||
}
|
|
@ -0,0 +1,367 @@
|
|||
<!-- ko if: working -->
|
||||
<div class="panel panel-dark fit">
|
||||
<spinner class="fit"></spinner>
|
||||
</div>
|
||||
<!-- /ko -->
|
||||
|
||||
<!-- ko ifnot: working -->
|
||||
|
||||
<div class="panel panel-dark animation-fade-in" data-bind="with: consoleOperation">
|
||||
<button type="button" class="no-border pull-right" data-dismiss="modal" aria-label="Close console"
|
||||
data-bind="click: $parents[1].closeConsole">
|
||||
<i class="icon-emb icon-emb-simple-remove"></i>
|
||||
</button>
|
||||
|
||||
<nav aria-label="breadcrumb" style="margin-right: 20px">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item">
|
||||
<a href="#" data-bind="text: api.displayName, attr: { href: $component.getApiReferenceUrl() }"></a>
|
||||
</li>
|
||||
<!-- ko if: api.apiVersion -->
|
||||
<li class="breadcrumb-item">
|
||||
<span data-bind="text: api.apiVersion"></span>
|
||||
</li>
|
||||
<!-- /ko -->
|
||||
<li class="breadcrumb-item">
|
||||
<span data-bind="text: name"></span>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div class="monospace" data-bind="text: urlTemplate, attr: { 'data-method': method }"></div>
|
||||
|
||||
|
||||
<!-- ko if: $component.hostnameSelectionEnabled -->
|
||||
<h3>Host</h3>
|
||||
|
||||
<div class="row flex flex-row">
|
||||
<div class="col-4">
|
||||
<label for="hostname" class="text-monospace form-label">
|
||||
Hostname
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="form-group">
|
||||
<select id="hostname" class="form-control" data-bind="value: $component.selectedHostname">
|
||||
<!-- ko foreach: { data: $component.hostnames, as: 'hostname' } -->
|
||||
<option data-bind="value: hostname, text: hostname"></option>
|
||||
<!-- /ko -->
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /ko -->
|
||||
|
||||
<!-- ko if: $component.isHostnameWildcarded -->
|
||||
<div class="row flex flex-row">
|
||||
<div class="col-4">
|
||||
<label for="wildcardSegment" class="text-monospace form-label">
|
||||
Wildcard segment
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="form-group">
|
||||
<input id="wildcardSegment" type="text" autocomplete="off" class="form-control form-control-sm"
|
||||
placeholder="name" spellcheck="false"
|
||||
data-bind="event: { keyup: $component.updateRequestSummary }, textInput: $component.wildcardSegment">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /ko -->
|
||||
|
||||
<!-- ko if: $component.authorizationServer() || $component.subscriptionKeyRequired() -->
|
||||
<h3>Authorization</h3>
|
||||
<!-- /ko -->
|
||||
|
||||
<!-- ko if: $component.authorizationServer -->
|
||||
<div class="row flex flex-row">
|
||||
<div class="col-4">
|
||||
<label for="authServer" class="text-monospace form-label"
|
||||
data-bind="text: $component.authorizationServer().displayName"></label>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="form-group">
|
||||
<select id="authServer" class="form-control"
|
||||
data-bind="options: $component.authorizationServer().grantTypes, value: $component.selectedGrantType, optionsCaption: 'No auth'">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /ko -->
|
||||
|
||||
<!-- ko if: $component.subscriptionKeyRequired -->
|
||||
<div class="row flex flex-row">
|
||||
<div class="col-4">
|
||||
<label for="subscriptionKey" class="text-monospace form-label">
|
||||
Subscription key
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="form-group">
|
||||
<!-- ko if: $component.products() && $component.products().length > 0 -->
|
||||
<select id="subscriptionKey" class="form-control" data-bind="value: $component.selectedSubscriptionKey">
|
||||
<!-- ko foreach: { data: $component.products, as: 'product' } -->
|
||||
<optgroup data-bind="attr: { label: product.name }">
|
||||
<!-- ko foreach: { data: product.subscriptionKeys, as: 'subscriptionKey' } -->
|
||||
<option data-bind="value: subscriptionKey.value, text: subscriptionKey.name"></option>
|
||||
<!-- /ko -->
|
||||
</optgroup>
|
||||
<!-- /ko -->
|
||||
</select>
|
||||
<!-- /ko -->
|
||||
<!-- ko if: !$component.products() || $component.products().length === 0 -->
|
||||
<input id="subscriptionKey" type="text" class="form-control" placeholder="subscription key"
|
||||
data-bind="textInput: $component.selectedSubscriptionKey" />
|
||||
<!-- /ko -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /ko -->
|
||||
|
||||
<h3>Parameters</h3>
|
||||
<!-- ko if: (templateParameters && templateParameters().length > 0) || (request.queryParameters && request.queryParameters().length > 0) -->
|
||||
<div data-bind="foreach: { data: templateParameters, as: 'parameter' }">
|
||||
<div class="row flex flex-row">
|
||||
<div class="col-4">
|
||||
<label class="text-monospace form-label" data-bind="text: parameter.name">
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="form-group">
|
||||
<!-- ko if: parameter.options.length > 0 -->
|
||||
<select class="form-control" aria-label="Parameter value"
|
||||
data-bind="value: parameter.value, options: parameter.options, optionsAfterRender: $component.updateRequestSummary, event:{ change: $component.updateRequestSummary }"></select>
|
||||
<!-- /ko -->
|
||||
<!-- ko if: parameter.options.length === 0 -->
|
||||
<div class="input-group">
|
||||
<input type="text" autocomplete="off" class="form-control form-control-sm" placeholder="value"
|
||||
spellcheck="false" aria-label="Parameter value"
|
||||
data-bind="event: { keyup: $component.updateRequestSummary }, textInput: parameter.value">
|
||||
<span class="invalid-feedback" data-bind="validationMessage: parameter.value"></span>
|
||||
</div>
|
||||
<!-- /ko -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div data-bind="foreach: { data: request.queryParameters, as: 'parameter' }">
|
||||
<div class="row flex flex-row">
|
||||
<div class="col-4">
|
||||
<div class="form-group">
|
||||
<div class="input-group">
|
||||
<input type="text" autocomplete="off" class="form-control form-control-sm" placeholder="name"
|
||||
spellcheck="false" aria-label="Parameter name" data-bind="textInput: parameter.name">
|
||||
<span class="invalid-feedback" data-bind="validationMessage: parameter.name"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="form-group">
|
||||
<!-- ko if: parameter.options.length > 0 -->
|
||||
<select class="form-control" aria-label="Parameter value"
|
||||
data-bind="value: parameter.value, options: parameter.options, optionsAfterRender: $component.updateRequestSummary, event:{ change: $component.updateRequestSummary }"></select>
|
||||
<!-- /ko -->
|
||||
<!-- ko if: parameter.options.length === 0 -->
|
||||
<input type="text" autocomplete="off" class="form-control form-control-sm" placeholder="value"
|
||||
spellcheck="false" aria-label="Parameter value"
|
||||
data-bind="event: { keyup: $component.updateRequestSummary }, textInput: parameter.value">
|
||||
<!-- /ko -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<a href="#" role="button" data-bind="click: $component.removeQueryParameter">Remove</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /ko -->
|
||||
|
||||
<a href="#" role="button" data-bind="click: $component.addQueryParameter">
|
||||
<i class="icon-emb icon-emb-plus"></i>
|
||||
Add parameter
|
||||
</a>
|
||||
|
||||
<h3>Headers</h3>
|
||||
|
||||
<!-- ko if: request.headers().length > 0 -->
|
||||
<div data-bind="foreach: { data: request.headers, as: 'header' }">
|
||||
<div class="row flex flex-row">
|
||||
<div class="col-4">
|
||||
<div class="form-group">
|
||||
<div class="input-group">
|
||||
<input type="text" autocomplete="off" class="form-control form-control-sm" placeholder="name"
|
||||
spellcheck="false" aria-label="Header name" data-bind="textInput: header.name">
|
||||
<span class="invalid-feedback" data-bind="validationMessage: header.name"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="form-group">
|
||||
<!-- ko if: header.options && header.options.length > 0 -->
|
||||
<select class="form-control" aria-label="Header value"
|
||||
data-bind="value: header.value, options: header.options, optionsAfterRender: $component.updateRequestSummary, event:{ change: $component.updateRequestSummary }"></select>
|
||||
<!-- /ko -->
|
||||
<!-- ko if: !header.options || header.options.length === 0 -->
|
||||
<div class="input-group">
|
||||
<input type="text" autocomplete="off" class="form-control form-control-sm" placeholder="value"
|
||||
spellcheck="false" aria-label="Header value"
|
||||
data-bind="event: { keyup: $component.updateRequestSummary }, textInput: header.value">
|
||||
<span class="invalid-feedback" data-bind="validationMessage: header.value"></span>
|
||||
</div>
|
||||
<!-- /ko -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<a href="#" role="button" data-bind="click: $component.removeHeader">Remove</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /ko -->
|
||||
|
||||
<a href="#" role="button" data-bind="click: $component.addHeader">
|
||||
<i class="icon-emb icon-emb-plus"></i> Add header
|
||||
</a>
|
||||
|
||||
<!-- ko if: hasBody -->
|
||||
<h3>Body</h3>
|
||||
|
||||
<div class="flex justify-content-end">
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="radio" name="bodyFormat" id="bodyFormatRaw" value="raw"
|
||||
data-bind="checked: request.bodyFormat">
|
||||
<label class="form-check-label" for="bodyFormatRaw">Raw</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="radio" name="bodyFormat" id="bodyFormatBinary" value="binary"
|
||||
data-bind="checked: request.bodyFormat">
|
||||
<label class="form-check-label" for="bodyFormatBinary">Binary</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<!-- ko if: request.bodyFormat() === 'raw' -->
|
||||
<textarea class="form-control form-control-sm" rows="5" aria-label="Request body"
|
||||
data-bind="event: { keyup: $component.updateRequestSummary }, textInput: request.body, valueUpdate: 'keyup'"></textarea>
|
||||
<!-- /ko -->
|
||||
<!-- ko if: request.bodyFormat() === 'binary' -->
|
||||
<file-input params="{ onSelect: $component.onFileSelect }" class="form-control" aria-label="Request body"
|
||||
data-bind="css: { 'is-invalid': !request.binary.isValid() }"></file-input>
|
||||
<span class="invalid-feedback" data-bind="validationMessage: request.binary"></span>
|
||||
<!-- /ko -->
|
||||
</div>
|
||||
<!-- /ko -->
|
||||
</div>
|
||||
|
||||
<div class="panel panel-dark animation-fade-in panel-highlight flex-item flex-grow menu menu-horizontal">
|
||||
<ul class="nav" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link" href="#" role="tab"
|
||||
data-bind="click: $component.selectedLanguage.bind(this, 'http'), css: { 'nav-link-active': $component.selectedLanguage() === 'http' }">
|
||||
HTTP
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link" href="#" role="tab"
|
||||
data-bind="click: $component.selectedLanguage.bind(this, 'curl'), css: { 'nav-link-active': $component.selectedLanguage() === 'curl' }">
|
||||
Curl
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link" href="#" role="tab"
|
||||
data-bind="click: $component.selectedLanguage.bind(this, 'csharp'), css: { 'nav-link-active': $component.selectedLanguage() === 'csharp' }">
|
||||
C#
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link" href="#" role="tab"
|
||||
data-bind="click: $component.selectedLanguage.bind(this, 'java'), css: { 'nav-link-active': $component.selectedLanguage() === 'java' }">
|
||||
Java
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link" href="#" role="tab"
|
||||
data-bind="click: $component.selectedLanguage.bind(this, 'javascript'), css: { 'nav-link-active': $component.selectedLanguage() === 'javascript' }">
|
||||
JavaScript
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link" href="#" role="tab"
|
||||
data-bind="click: $component.selectedLanguage.bind(this, 'php'), css: { 'nav-link-active': $component.selectedLanguage() === 'php' }">
|
||||
PHP
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link" href="#" role="tab"
|
||||
data-bind="click: $component.selectedLanguage.bind(this, 'python'), css: { 'nav-link-active': $component.selectedLanguage() === 'python' }">
|
||||
Python
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link" href="#" role="tab"
|
||||
data-bind="click: $component.selectedLanguage.bind(this, 'ruby'), css: { 'nav-link-active': $component.selectedLanguage() === 'ruby' }">
|
||||
Ruby
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link" href="#" role="tab"
|
||||
data-bind="click: $component.selectedLanguage.bind(this, 'objc'), css: { 'nav-link-active': $component.selectedLanguage() === 'objc' }">
|
||||
Objective C
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3>HTTP request</h3>
|
||||
<div class="code-block">
|
||||
<div class="code-block-heading">
|
||||
<button class="code-block-command" data-bind="copyToClipboard: $component.codeSample"
|
||||
aria-label="Copy to clipboard">
|
||||
<i class="icon-emb icon-emb-duplicate"></i> Copy
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ko if: $component.codeSample -->
|
||||
<pre data-bind="syntaxHighlight: { code: $component.codeSample, language: $component.selectedLanguage }"></pre>
|
||||
<!-- /ko -->
|
||||
|
||||
<div class="flex flex-column align-items-end">
|
||||
<div class="btn-group" role="group">
|
||||
<!-- ko ifnot: sendingRequest -->
|
||||
<button type="button" class="button button-primary btn-sm" data-bind="click: validateAndSendRequest">
|
||||
Send
|
||||
</button>
|
||||
<!-- /ko -->
|
||||
<!-- ko if: sendingRequest -->
|
||||
<button type="button" class="button button-primary btn-sm" disabled>
|
||||
Sending...
|
||||
</button>
|
||||
<!-- /ko -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ko if: sendingRequest -->
|
||||
<div class="panel panel-dark" data-bind="scrollintoview: {}">
|
||||
<spinner class="fit"></spinner>
|
||||
</div>
|
||||
<!-- /ko -->
|
||||
|
||||
<!-- ko if: requestError -->
|
||||
<div class="panel panel-dark animation-fade-in" data-bind="scrollintoview: {}">
|
||||
<p>Unable to complete the request</p>
|
||||
<p class="text-muted" data-bind="html: requestError"></p>
|
||||
</div>
|
||||
<!-- /ko -->
|
||||
|
||||
<!-- ko if: responseStatusCode -->
|
||||
<div class="panel panel-dark menu menu-horizontal animation-fade-in" data-bind="scrollintoview: {}">
|
||||
|
||||
<h3>HTTP response</h3>
|
||||
<pre><span>HTTP/1.1</span> <span data-bind="attr: { 'data-code': responseStatusCode }"><span data-bind="text: responseStatusCode, attr: { 'data-code':responseStatusCode }"></span> <span data-bind="text: responseStatusText"></span></span>
|
||||
|
||||
<span data-bind="text: responseHeadersString"></span>
|
||||
|
||||
<span data-bind="text: responseBody"></span></pre>
|
||||
|
||||
</div>
|
||||
<!--/ko-->
|
||||
|
||||
<!--/ko-->
|
|
@ -0,0 +1,547 @@
|
|||
import * as ko from "knockout";
|
||||
import * as validation from "knockout.validation";
|
||||
import * as _ from "lodash";
|
||||
import template from "./operation-console.html";
|
||||
import { Component, Param, OnMounted } from "@paperbits/common/ko/decorators";
|
||||
import { Operation } from "../../../../../models/operation";
|
||||
import { ApiService } from "../../../../../services/apiService";
|
||||
import { ConsoleOperation } from "../../../../../models/console/consoleOperation";
|
||||
import { ConsoleHeader } from "../../../../../models/console/consoleHeader";
|
||||
import { Utils } from "../../../../../utils";
|
||||
import { KnownHttpHeaders } from "../../../../../models/knownHttpHeaders";
|
||||
import { Api } from "../../../../../models/api";
|
||||
import { KnownStatusCodes } from "../../../../../models/knownStatusCodes";
|
||||
import { Product } from "../../../../../models/product";
|
||||
import { ServiceSkuName, TypeOfApi } from "../../../../../constants";
|
||||
import { HttpClient, HttpRequest } from "@paperbits/common/http";
|
||||
import { Revision } from "../../../../../models/revision";
|
||||
import { templates } from "./templates/templates";
|
||||
import { ConsoleParameter } from "../../../../../models/console/consoleParameter";
|
||||
import { RouteHelper } from "../../../../../routing/routeHelper";
|
||||
import { TemplatingService } from "../../../../../services/templatingService";
|
||||
import { OAuthService } from "../../../../../services/oauthService";
|
||||
import { AuthorizationServer } from "../../../../../models/authorizationServer";
|
||||
import { SessionManager } from "../../../../../authentication/sessionManager";
|
||||
import { OAuthSession, StoredCredentials } from "./oauthSession";
|
||||
|
||||
const oauthSessionKey = "oauthSession";
|
||||
|
||||
@Component({
|
||||
selector: "operation-console",
|
||||
template: template
|
||||
})
|
||||
export class OperationConsole {
|
||||
public readonly sendingRequest: ko.Observable<boolean>;
|
||||
public readonly working: ko.Observable<boolean>;
|
||||
public readonly consoleOperation: ko.Observable<ConsoleOperation>;
|
||||
public readonly secretsRevealed: ko.Observable<boolean>;
|
||||
public readonly responseStatusCode: ko.Observable<string>;
|
||||
public readonly responseStatusText: ko.Observable<string>;
|
||||
public readonly responseBody: ko.Observable<string>;
|
||||
public readonly responseHeadersString: ko.Observable<string>;
|
||||
public readonly products: ko.Observable<Product[]>;
|
||||
public readonly selectedSubscriptionKey: ko.Observable<string>;
|
||||
public readonly subscriptionKeyRequired: ko.Observable<boolean>;
|
||||
public readonly selectedLanguage: ko.Observable<string>;
|
||||
public readonly selectedProduct: ko.Observable<Product>;
|
||||
public readonly requestError: ko.Observable<string>;
|
||||
public readonly codeSample: ko.Observable<string>;
|
||||
public readonly selectedHostname: ko.Observable<string>;
|
||||
public readonly isHostnameWildcarded: ko.Computed<boolean>;
|
||||
public readonly hostnameSelectionEnabled: ko.Observable<boolean>;
|
||||
public readonly wildcardSegment: ko.Observable<string>;
|
||||
public readonly selectedGrantType: ko.Observable<string>;
|
||||
public isConsumptionMode: boolean;
|
||||
public templates: Object;
|
||||
|
||||
constructor(
|
||||
private readonly apiService: ApiService,
|
||||
private readonly httpClient: HttpClient,
|
||||
private readonly routeHelper: RouteHelper,
|
||||
private readonly oauthService: OAuthService,
|
||||
private readonly sessionManager: SessionManager
|
||||
) {
|
||||
this.templates = templates;
|
||||
this.products = ko.observable();
|
||||
|
||||
this.requestError = ko.observable();
|
||||
this.responseStatusCode = ko.observable();
|
||||
this.responseStatusText = ko.observable();
|
||||
this.responseHeadersString = ko.observable();
|
||||
this.responseBody = ko.observable();
|
||||
this.selectedLanguage = ko.observable("http");
|
||||
this.api = ko.observable<Api>();
|
||||
this.revision = ko.observable();
|
||||
this.operation = ko.observable();
|
||||
this.hostnames = ko.observable();
|
||||
this.consoleOperation = ko.observable();
|
||||
this.secretsRevealed = ko.observable();
|
||||
this.selectedSubscriptionKey = ko.observable();
|
||||
this.subscriptionKeyRequired = ko.observable();
|
||||
this.working = ko.observable(true);
|
||||
this.sendingRequest = ko.observable(false);
|
||||
this.codeSample = ko.observable();
|
||||
this.selectedProduct = ko.observable();
|
||||
this.onFileSelect = this.onFileSelect.bind(this);
|
||||
this.selectedHostname = ko.observable("");
|
||||
this.hostnameSelectionEnabled = ko.observable();
|
||||
this.isHostnameWildcarded = ko.computed(() => this.selectedHostname().includes("*"));
|
||||
this.selectedGrantType = ko.observable();
|
||||
this.authorizationServer = ko.observable();
|
||||
|
||||
this.wildcardSegment = ko.observable();
|
||||
|
||||
validation.rules["maxFileSize"] = {
|
||||
validator: (file: File, maxSize: number) => !file || file.size < maxSize,
|
||||
message: (size) => `The file size cannot exceed ${Utils.formatBytes(size)}.`
|
||||
};
|
||||
|
||||
validation.registerExtenders();
|
||||
|
||||
validation.init({
|
||||
insertMessages: false,
|
||||
errorElementClass: "is-invalid",
|
||||
decorateInputElement: true
|
||||
});
|
||||
}
|
||||
|
||||
@Param()
|
||||
public api: ko.Observable<Api>;
|
||||
|
||||
@Param()
|
||||
public operation: ko.Observable<Operation>;
|
||||
|
||||
@Param()
|
||||
public revision: ko.Observable<Revision>;
|
||||
|
||||
@Param()
|
||||
public hostnames: ko.Observable<string[]>;
|
||||
|
||||
@Param()
|
||||
public authorizationServer: ko.Observable<AuthorizationServer>;
|
||||
|
||||
@OnMounted()
|
||||
public async initialize(): Promise<void> {
|
||||
await this.resetConsole();
|
||||
|
||||
this.selectedHostname.subscribe(this.setHostname);
|
||||
this.wildcardSegment.subscribe((wildcardSegment) => {
|
||||
const hostname = wildcardSegment
|
||||
? this.selectedHostname().replace("*", wildcardSegment)
|
||||
: this.selectedHostname();
|
||||
|
||||
this.setHostname(hostname);
|
||||
});
|
||||
this.selectedSubscriptionKey.subscribe(this.applySubscriptionKey.bind(this));
|
||||
this.api.subscribe(this.resetConsole);
|
||||
this.operation.subscribe(this.resetConsole);
|
||||
this.selectedLanguage.subscribe(this.updateRequestSummary);
|
||||
this.selectedGrantType.subscribe(this.authenticateOAuth);
|
||||
}
|
||||
|
||||
private async resetConsole(): Promise<void> {
|
||||
const selectedOperation = this.operation();
|
||||
const selectedApi = this.api();
|
||||
|
||||
if (!selectedApi || !selectedOperation) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.working(true);
|
||||
this.sendingRequest(false);
|
||||
this.consoleOperation(null);
|
||||
this.secretsRevealed(false);
|
||||
this.responseStatusCode(null);
|
||||
this.responseStatusText(null);
|
||||
this.responseBody(null);
|
||||
this.selectedSubscriptionKey(null);
|
||||
this.subscriptionKeyRequired(!!selectedApi.subscriptionRequired);
|
||||
this.selectedProduct(null);
|
||||
|
||||
const operation = await this.apiService.getOperation(selectedApi.name, selectedOperation.name);
|
||||
const consoleOperation = new ConsoleOperation(selectedApi, operation);
|
||||
this.consoleOperation(consoleOperation);
|
||||
|
||||
const hostnames = this.hostnames();
|
||||
this.hostnameSelectionEnabled(this.hostnames()?.length > 1);
|
||||
|
||||
const hostname = hostnames[0];
|
||||
this.selectedHostname(hostname);
|
||||
|
||||
this.hostnameSelectionEnabled(this.hostnames()?.length > 1);
|
||||
consoleOperation.host.hostname(hostname);
|
||||
|
||||
if (this.api().type === TypeOfApi.soap) {
|
||||
this.setSoapHeaders();
|
||||
}
|
||||
|
||||
if (!this.isConsumptionMode) {
|
||||
this.setNoCacheHeader();
|
||||
}
|
||||
|
||||
if (this.api().apiVersionSet && this.api().apiVersionSet.versioningScheme === "Header") {
|
||||
this.setVersionHeader();
|
||||
}
|
||||
|
||||
await this.setupOAuth();
|
||||
|
||||
this.updateRequestSummary();
|
||||
this.working(false);
|
||||
}
|
||||
|
||||
private setSoapHeaders(): void {
|
||||
const consoleOperation = this.consoleOperation();
|
||||
const representation = consoleOperation.request.representations[0];
|
||||
|
||||
if (representation) {
|
||||
if (representation.contentType.toLowerCase() === "text/xml".toLowerCase()) {
|
||||
// Soap 1.1
|
||||
consoleOperation.setHeader(KnownHttpHeaders.SoapAction, `"${consoleOperation.urlTemplate.split("=")[1]}"`);
|
||||
}
|
||||
|
||||
if (representation.contentType.toLowerCase() === "application/soap+xml".toLowerCase()) {
|
||||
// Soap 1.2
|
||||
const contentHeader = consoleOperation.request.headers()
|
||||
.find(header => header.name().toLowerCase() === KnownHttpHeaders.ContentType.toLowerCase());
|
||||
|
||||
if (contentHeader) {
|
||||
const contentType = `${contentHeader.value};action="${consoleOperation.urlTemplate.split("=")[1]}"`;
|
||||
consoleOperation.setHeader(KnownHttpHeaders.ContentType, contentType);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
consoleOperation.setHeader(KnownHttpHeaders.SoapAction, "\"" + consoleOperation.urlTemplate.split("=")[1] + "\"");
|
||||
}
|
||||
|
||||
consoleOperation.urlTemplate = "";
|
||||
}
|
||||
|
||||
private setHostname(hostname: string): void {
|
||||
this.consoleOperation().host.hostname(hostname);
|
||||
this.updateRequestSummary();
|
||||
}
|
||||
|
||||
public addHeader(): void {
|
||||
this.consoleOperation().request.headers.push(new ConsoleHeader());
|
||||
this.updateRequestSummary();
|
||||
}
|
||||
|
||||
public removeHeader(header: ConsoleHeader): void {
|
||||
this.consoleOperation().request.headers.remove(header);
|
||||
this.updateRequestSummary();
|
||||
}
|
||||
|
||||
private findHeader(name: string): ConsoleHeader {
|
||||
const searchName = name.toLocaleLowerCase();
|
||||
|
||||
return this.consoleOperation().request
|
||||
.headers()
|
||||
.find(x => x.name()?.toLocaleLowerCase() === searchName);
|
||||
}
|
||||
|
||||
public addQueryParameter(): void {
|
||||
this.consoleOperation().request.queryParameters.push(new ConsoleParameter());
|
||||
this.updateRequestSummary();
|
||||
}
|
||||
|
||||
public removeQueryParameter(parameter: ConsoleParameter): void {
|
||||
this.consoleOperation().request.queryParameters.remove(parameter);
|
||||
this.updateRequestSummary();
|
||||
}
|
||||
|
||||
private applySubscriptionKey(subscriptionKey: string): void {
|
||||
if (!this.consoleOperation()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setSubscriptionKeyHeader(subscriptionKey);
|
||||
this.updateRequestSummary();
|
||||
}
|
||||
|
||||
private setNoCacheHeader(): void {
|
||||
this.consoleOperation().setHeader(KnownHttpHeaders.CacheControl, "no-cache", "string", "Disable caching.");
|
||||
}
|
||||
|
||||
private setVersionHeader(): void {
|
||||
this.consoleOperation().setHeader(this.api().apiVersionSet.versionHeaderName, this.api().apiVersion, "string", "API version");
|
||||
}
|
||||
|
||||
private getSubscriptionKeyHeaderName(): string {
|
||||
let subscriptionKeyHeaderName = KnownHttpHeaders.OcpApimSubscriptionKey;
|
||||
|
||||
if (this.api().subscriptionKeyParameterNames && this.api().subscriptionKeyParameterNames.header) {
|
||||
subscriptionKeyHeaderName = this.api().subscriptionKeyParameterNames.header;
|
||||
}
|
||||
|
||||
return subscriptionKeyHeaderName;
|
||||
}
|
||||
|
||||
private getSubscriptionKeyHeader(): ConsoleHeader {
|
||||
const subscriptionKeyHeaderName = this.getSubscriptionKeyHeaderName();
|
||||
return this.findHeader(subscriptionKeyHeaderName);
|
||||
}
|
||||
|
||||
private setSubscriptionKeyHeader(subscriptionKey: string): void {
|
||||
this.removeSubscriptionKeyHeader();
|
||||
|
||||
if (!subscriptionKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const subscriptionKeyHeaderName = this.getSubscriptionKeyHeaderName();
|
||||
|
||||
const keyHeader = new ConsoleHeader();
|
||||
keyHeader.name(subscriptionKeyHeaderName);
|
||||
keyHeader.value(subscriptionKey);
|
||||
keyHeader.description = "Subscription key.";
|
||||
keyHeader.secret = true;
|
||||
keyHeader.inputTypeValue = "password";
|
||||
keyHeader.type = "string";
|
||||
keyHeader.required = true;
|
||||
|
||||
this.consoleOperation().request.headers.push(keyHeader);
|
||||
this.updateRequestSummary();
|
||||
}
|
||||
|
||||
private removeAuthorizationHeader(): void {
|
||||
const authorizationHeader = this.findHeader(KnownHttpHeaders.Authorization);
|
||||
this.removeHeader(authorizationHeader);
|
||||
}
|
||||
|
||||
private setAuthorizationHeader(accessToken: string): void {
|
||||
this.removeAuthorizationHeader();
|
||||
|
||||
const keyHeader = new ConsoleHeader();
|
||||
keyHeader.name(KnownHttpHeaders.Authorization);
|
||||
keyHeader.value(accessToken);
|
||||
keyHeader.description = "Subscription key.";
|
||||
keyHeader.secret = true;
|
||||
keyHeader.inputTypeValue = "password";
|
||||
keyHeader.type = "string";
|
||||
keyHeader.required = true;
|
||||
|
||||
this.consoleOperation().request.headers.push(keyHeader);
|
||||
this.updateRequestSummary();
|
||||
}
|
||||
|
||||
private removeSubscriptionKeyHeader(): void {
|
||||
const subscriptionKeyHeader = this.getSubscriptionKeyHeader();
|
||||
this.removeHeader(subscriptionKeyHeader);
|
||||
}
|
||||
|
||||
public async updateRequestSummary(): Promise<void> {
|
||||
const template = templates[this.selectedLanguage()];
|
||||
const codeSample = await TemplatingService.render(template, ko.toJS(this.consoleOperation));
|
||||
|
||||
this.codeSample(codeSample);
|
||||
}
|
||||
|
||||
public onFileSelect(file: File): void {
|
||||
this.consoleOperation().request.binary(file);
|
||||
this.updateRequestSummary();
|
||||
}
|
||||
|
||||
public async validateAndSendRequest(): Promise<void> {
|
||||
const operation = this.consoleOperation();
|
||||
const templateParameters = operation.templateParameters();
|
||||
const queryParameters = operation.request.queryParameters();
|
||||
const headers = operation.request.headers();
|
||||
const binary = operation.request.binary;
|
||||
const parameters = [].concat(templateParameters, queryParameters, headers);
|
||||
const validationGroup = validation.group(parameters.map(x => x.value).concat(binary), { live: true });
|
||||
const clientErrors = validationGroup();
|
||||
|
||||
if (clientErrors.length > 0) {
|
||||
validationGroup.showAllMessages();
|
||||
return;
|
||||
}
|
||||
|
||||
this.sendRequest();
|
||||
}
|
||||
|
||||
private async sendRequest(): Promise<void> {
|
||||
this.requestError(null);
|
||||
this.sendingRequest(true);
|
||||
this.responseStatusCode(null);
|
||||
|
||||
const consoleOperation = this.consoleOperation();
|
||||
const request = consoleOperation.request;
|
||||
const url = consoleOperation.requestUrl();
|
||||
const method = consoleOperation.method;
|
||||
const headers = [...request.headers()];
|
||||
|
||||
let payload;
|
||||
|
||||
switch (consoleOperation.request.bodyFormat()) {
|
||||
case "raw":
|
||||
payload = request.body();
|
||||
break;
|
||||
|
||||
case "binary":
|
||||
payload = await Utils.readFileAsByteArray(request.binary());
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error("Unknown body format.");
|
||||
}
|
||||
|
||||
try {
|
||||
const request: HttpRequest = {
|
||||
url: url,
|
||||
method: method,
|
||||
headers: headers
|
||||
.map(x => { return { name: x.name(), value: x.value() ?? "" }; })
|
||||
.filter(x => !!x.name && !!x.value),
|
||||
body: payload
|
||||
};
|
||||
|
||||
const response = await this.httpClient.send(request);
|
||||
this.responseHeadersString(response.headers.map(x => `${x.name}: ${x.value}`).join("\n"));
|
||||
|
||||
const knownStatusCode = KnownStatusCodes.find(x => x.code === response.statusCode);
|
||||
|
||||
const responseStatusText = knownStatusCode
|
||||
? knownStatusCode.description
|
||||
: "Unknown";
|
||||
|
||||
this.responseStatusCode(response.statusCode.toString());
|
||||
this.responseStatusText(responseStatusText);
|
||||
this.responseBody(response.toText());
|
||||
|
||||
const responseHeaders = response.headers.map(x => {
|
||||
const consoleHeader = new ConsoleHeader();
|
||||
consoleHeader.name(x.name);
|
||||
consoleHeader.value(x.value);
|
||||
return consoleHeader;
|
||||
});
|
||||
|
||||
const contentTypeHeader = responseHeaders.find((header) => header.name().toLowerCase() === KnownHttpHeaders.ContentType.toLowerCase());
|
||||
|
||||
if (contentTypeHeader) {
|
||||
if (contentTypeHeader.value().toLowerCase().indexOf("json") >= 0) {
|
||||
this.responseBody(Utils.formatJson(this.responseBody()));
|
||||
}
|
||||
|
||||
if (contentTypeHeader.value().toLowerCase().indexOf("xml") >= 0) {
|
||||
this.responseBody(Utils.formatXml(this.responseBody()));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
if (error.code && error.code === "RequestError") {
|
||||
this.requestError(`Since the browser initiates the request, it requires Cross-Origin Resource Sharing (CORS) enabled on the server. <a href="https://aka.ms/AA4e482" target="_blank">Learn more</a>`);
|
||||
}
|
||||
}
|
||||
finally {
|
||||
this.sendingRequest(false);
|
||||
}
|
||||
}
|
||||
|
||||
public toggleRequestSummarySecrets(): void {
|
||||
this.secretsRevealed(!this.secretsRevealed());
|
||||
}
|
||||
|
||||
public getApiReferenceUrl(): string {
|
||||
return this.routeHelper.getApiReferenceUrl(this.api().name);
|
||||
}
|
||||
|
||||
private getSessionRecordKey(authorizationServerName: string, scopeOverride: string): string {
|
||||
let recordKey = authorizationServerName;
|
||||
|
||||
if (scopeOverride) {
|
||||
recordKey += `-${scopeOverride}`;
|
||||
}
|
||||
|
||||
return recordKey;
|
||||
}
|
||||
|
||||
private async setupOAuth(): Promise<void> {
|
||||
const authorizationServer = this.authorizationServer();
|
||||
|
||||
if (!authorizationServer) {
|
||||
this.selectedGrantType(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const api = this.api();
|
||||
const scopeOverride = api.authenticationSettings?.oAuth2?.scope;
|
||||
const storedCredentials = await this.getStoredCredentials(authorizationServer.name, scopeOverride);
|
||||
|
||||
if (storedCredentials) {
|
||||
this.selectedGrantType(storedCredentials.grantType);
|
||||
this.setAuthorizationHeader(storedCredentials.accessToken);
|
||||
}
|
||||
}
|
||||
|
||||
private async getStoredCredentials(serverName: string, scopeOverride: string): Promise<StoredCredentials> {
|
||||
const oauthSession = await this.sessionManager.getItem<OAuthSession>(oauthSessionKey);
|
||||
const recordKey = this.getSessionRecordKey(serverName, scopeOverride);
|
||||
const storedCredentials = oauthSession?.[recordKey];
|
||||
|
||||
try {
|
||||
/* Trying to check if it's a JWT token and, if yes, whether it got expired. */
|
||||
const jwtToken = Utils.parseJwt(storedCredentials.accessToken.replace(/^bearer /i, ""));
|
||||
const now = Utils.getUtcDateTime();
|
||||
|
||||
if (now > jwtToken.exp) {
|
||||
await this.clearStoredCredentials();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
return storedCredentials;
|
||||
}
|
||||
|
||||
private async setStoredCredentials(serverName: string, scopeOverride: string, grantType: string, accessToken: string): Promise<void> {
|
||||
const oauthSession = await this.sessionManager.getItem<OAuthSession>(oauthSessionKey) || {};
|
||||
const recordKey = this.getSessionRecordKey(serverName, scopeOverride);
|
||||
|
||||
oauthSession[recordKey] = {
|
||||
grantType: grantType,
|
||||
accessToken: accessToken
|
||||
};
|
||||
|
||||
await this.sessionManager.setItem<object>(oauthSessionKey, oauthSession);
|
||||
}
|
||||
|
||||
private async clearStoredCredentials(): Promise<void> {
|
||||
await this.sessionManager.removeItem(oauthSessionKey);
|
||||
this.removeAuthorizationHeader();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates specified authentication flow.
|
||||
* @param grantType OAuth grant type, e.g. "implicit" or "authorization_code".
|
||||
*/
|
||||
public async authenticateOAuth(grantType: string): Promise<void> {
|
||||
await this.clearStoredCredentials();
|
||||
|
||||
if (!grantType) {
|
||||
return;
|
||||
}
|
||||
|
||||
const api = this.api();
|
||||
const authorizationServer = this.authorizationServer();
|
||||
const scopeOverride = api.authenticationSettings?.oAuth2?.scope;
|
||||
const serverName = authorizationServer.name;
|
||||
const storedCredentials = await this.getStoredCredentials(serverName, scopeOverride);
|
||||
|
||||
if (storedCredentials) {
|
||||
this.setAuthorizationHeader(storedCredentials.accessToken);
|
||||
return;
|
||||
}
|
||||
|
||||
if (scopeOverride) {
|
||||
authorizationServer.scopes = [scopeOverride];
|
||||
}
|
||||
|
||||
const accessToken = await this.oauthService.authenticate(grantType, authorizationServer);
|
||||
await this.setStoredCredentials(serverName, scopeOverride, grantType, accessToken);
|
||||
|
||||
this.setAuthorizationHeader(accessToken);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,204 @@
|
|||
<!-- ko if: working -->
|
||||
<spinner class="fit"></spinner>
|
||||
<!-- /ko -->
|
||||
|
||||
<!-- ko ifnot: working -->
|
||||
<!-- ko if: operation -->
|
||||
<div class="animation-fade-in">
|
||||
<div class="operation-header">
|
||||
<h2 class="operation-name" data-bind="click: $component.openConsole">
|
||||
<span data-bind="text: operation().displayName"></span>
|
||||
</h2>
|
||||
<!-- ko if: $component.enableConsole -->
|
||||
<button class="open-console-button" data-bind="click: $component.openConsole">Try it <i
|
||||
class="icon-emb icon-emb-play"></i>
|
||||
</button>
|
||||
<!-- /ko -->
|
||||
</div>
|
||||
|
||||
<!-- ko if: operation().description -->
|
||||
<div class="text-word-break" data-bind="markdown: operation().description"></div>
|
||||
<!-- /ko -->
|
||||
|
||||
<!-- ko if: tags().length > 0 -->
|
||||
<div class="tag-group">
|
||||
<!-- ko foreach: { data: $component.tags, as: 'tag' } -->
|
||||
<span class="tag-item" role="group" data-bind="text: tag"></span>
|
||||
<!-- /ko -->
|
||||
</div>
|
||||
<!-- /ko -->
|
||||
|
||||
<div class="collapsible">
|
||||
<h3>Request</h3>
|
||||
<div class="collapsible-container">
|
||||
<span class="monospace"
|
||||
data-bind="text: $component.requestUrlSample, attr: { 'data-method': operation().method }"></span>
|
||||
|
||||
<!-- ko if: operation().parameters.length > 0 -->
|
||||
<h4>Request parameters</h4>
|
||||
|
||||
<div role="table" class="table-preset table-preset-params">
|
||||
<div class="d-contents" role="rowgroup">
|
||||
<div class="d-contents" role="row">
|
||||
<div role="columnheader" class="table-preset-head text-truncate">Name</div>
|
||||
<div role="columnheader" class="table-preset-head text-truncate">In</div>
|
||||
<div role="columnheader" class="table-preset-head text-truncate">Required</div>
|
||||
<div role="columnheader" class="table-preset-head text-truncate">Type</div>
|
||||
<div role="columnheader" class="table-preset-head">Description</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-contents" role="rowgroup">
|
||||
<!-- ko foreach: { data: operation().parameters, as: 'parameter' } -->
|
||||
<div class="d-contents" role="row">
|
||||
<div role="cell" class="text-truncate monospace"
|
||||
data-bind="text: parameter.name, attr: { title: parameter.name }"></div>
|
||||
<div role="cell" class="text-truncate" data-bind="text: parameter.in"></div>
|
||||
<div role="cell" class="text-truncate" data-bind="text: parameter.required"></div>
|
||||
<div role="cell" class="text-truncate monospace" data-bind="text: parameter.type"></div>
|
||||
<div role="cell" data-bind="markdown: parameter.description"></div>
|
||||
</div>
|
||||
<!-- /ko -->
|
||||
</div>
|
||||
</div>
|
||||
<!-- /ko -->
|
||||
|
||||
<!-- ko if: operation().request && operation().request.headers && operation().request.headers.length > 0 -->
|
||||
<h4>Request headers</h4>
|
||||
|
||||
<div role="table" class="table-preset table-preset-headers">
|
||||
<div class="d-contents" role="rowgroup">
|
||||
<div class="d-contents" role="row">
|
||||
<div role="columnheader" class="table-preset-head text-truncate">Name</div>
|
||||
<div role="columnheader" class="table-preset-head text-truncate">Required</div>
|
||||
<div role="columnheader" class="table-preset-head text-truncate">Type</div>
|
||||
<div role="columnheader" class="table-preset-head">Description</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-contents" role="rowgroup">
|
||||
<!-- ko foreach: { data: operation().request.headers, as: 'header' } -->
|
||||
<div class="d-contents" role="row">
|
||||
<div role="cell" class="text-truncate monospace"
|
||||
data-bind="text: header.name, attr: { title: header.name }"></div>
|
||||
<div role="cell" class="text-truncate" data-bind="text: header.required"></div>
|
||||
<div role="cell" class="text-truncate monospace"
|
||||
data-bind="text: header.type, attr: { title: header.type }"></div>
|
||||
<div role="cell" data-bind="markdown: header.description"></div>
|
||||
</div>
|
||||
<!-- /ko -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- /ko -->
|
||||
|
||||
|
||||
<!-- ko with: operation().request, as: 'request' -->
|
||||
<!-- ko if: request.isMeaningful() -->
|
||||
<h4>Request body</h4>
|
||||
|
||||
<!-- ko if: request.description -->
|
||||
<div class="text-word-break" data-bind="markdown: request.description"></div>
|
||||
<!-- /ko -->
|
||||
|
||||
<!-- ko if: request.meaningfulRepresentations().length > 0 -->
|
||||
<div class="tabs" data-bind="foreach: { data: request.meaningfulRepresentations(), as: 'representation' }">
|
||||
<input class="tab-radio" type="radio" name="requestContentType"
|
||||
data-bind="attr : { id: 'request' + representation.contentType, checked: $index() === 0 }">
|
||||
|
||||
<label class="tab-label"
|
||||
data-bind="attr : { for: 'request' + representation.contentType }, text: representation.contentType"></label>
|
||||
|
||||
<div class="tab-content">
|
||||
<!-- ko if: representation.typeName -->
|
||||
<type-definition
|
||||
params="{ apiName: $component.api().name, operationName: $component.operation().name, definition: $component.getDefinitionForRepresentation(representation), defaultSchemaView: $component.defaultSchemaView }">
|
||||
</type-definition>
|
||||
<!-- /ko -->
|
||||
|
||||
<!-- ko ifnot: representation.typeName -->
|
||||
<!-- ko if: representation.example -->
|
||||
<code-sample params="{ content: representation.example, language: representation.exampleFormat }">
|
||||
</code-sample>
|
||||
<!-- /ko -->
|
||||
<!-- /ko -->
|
||||
</div>
|
||||
</div>
|
||||
<!-- /ko -->
|
||||
<!-- /ko -->
|
||||
|
||||
<!-- /ko -->
|
||||
|
||||
<!-- ko foreach: { data: operation().getMeaningfulResponses(), as: 'response' } -->
|
||||
<h3>Response: <span data-bind="text: response.statusCode"></span></h3>
|
||||
<!-- ko if: response.description -->
|
||||
<p data-bind="markdown: response.description"></p>
|
||||
<!-- /ko -->
|
||||
|
||||
<!-- ko if: response.meaningfulRepresentations().length > 0 -->
|
||||
<div class="tabs" data-bind="foreach: { data: meaningfulRepresentations(), as: 'representation' }">
|
||||
<input class="tab-radio" type="radio"
|
||||
data-bind="attr: { id: response.identifier + representation.contentType, name: response.identifier, checked: $index() === 0 }">
|
||||
|
||||
<label class="tab-label"
|
||||
data-bind="attr: { for: response.identifier + representation.contentType }, text: representation.contentType"></label>
|
||||
|
||||
<div class="tab-content">
|
||||
<!-- ko if: representation.typeName -->
|
||||
<type-definition
|
||||
params="{ apiName: $component.api().name, operationName: $component.operation().name, definition: $component.getDefinitionForRepresentation(representation), defaultSchemaView: $component.defaultSchemaView }">
|
||||
</type-definition>
|
||||
<!-- /ko -->
|
||||
<!-- ko ifnot: representation.typeName -->
|
||||
<!-- ko if: representation.example -->
|
||||
<code-sample params="{ content: representation.example, language: representation.exampleFormat }">
|
||||
</code-sample>
|
||||
<!-- /ko -->
|
||||
<!-- /ko -->
|
||||
</div>
|
||||
</div>
|
||||
<!-- /ko -->
|
||||
<!-- /ko -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ko if: $component.definitions().length > 0 -->
|
||||
<h3>Definitions</h3>
|
||||
|
||||
<div role="table" class="table-preset table-preset-definitions">
|
||||
<div class="d-contents" role="rowgroup">
|
||||
<div class="d-contents" role="row">
|
||||
<div class="table-preset-head text-truncate" role="columnheader">Name</div>
|
||||
<div class="table-preset-head text-truncate" role="columnheader">Description</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-contents" role="rowgroup">
|
||||
<!-- ko foreach: { data: definitions, as: 'definition' } -->
|
||||
<div class="d-contents" role="row">
|
||||
<div role="cell" class="text-truncate">
|
||||
<a data-bind="text: definition.name, attr: { title: definition.name, href: $component.getDefinitionReferenceUrl(definition) }"></a>
|
||||
</div>
|
||||
<div role="cell" data-bind="markdown: definition.description"></div>
|
||||
</div>
|
||||
<!-- /ko -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ko foreach: { data: definitions, as: 'definition' } -->
|
||||
<type-definition
|
||||
params="{ apiName: $component.api().name, operationName: $component.operation().name, definition: definition, anchor: true, defaultSchemaView: $component.defaultSchemaView }">
|
||||
</type-definition>
|
||||
<!-- /ko -->
|
||||
<!-- /ko -->
|
||||
</div>
|
||||
<!-- /ko -->
|
||||
<!-- ko ifnot: operation -->
|
||||
<p>No operation selected.</p>
|
||||
<!-- /ko -->
|
||||
<!-- /ko -->
|
||||
|
||||
<!-- ko if: $component.consoleIsOpen -->
|
||||
<div class="detachable-right scrollable flex-grow animation-fade-in">
|
||||
<operation-console class="test flex flex-column"
|
||||
params="{ api: api, operation: operation, hostnames: hostnames, authorizationServer: associatedAuthServer }">
|
||||
</operation-console>
|
||||
</div>
|
||||
<!-- /ko -->
|
|
@ -0,0 +1,320 @@
|
|||
import * as ko from "knockout";
|
||||
import template from "./operation-details.html";
|
||||
import { Router } from "@paperbits/common/routing";
|
||||
import { Component, RuntimeComponent, OnMounted, OnDestroyed, Param } from "@paperbits/common/ko/decorators";
|
||||
import { Api } from "../../../../../models/api";
|
||||
import { Operation } from "../../../../../models/operation";
|
||||
import { ApiService } from "../../../../../services/apiService";
|
||||
import { TypeDefinitionPropertyTypeCombination } from "./../../../../../models/typeDefinition";
|
||||
import { AuthorizationServer } from "./../../../../../models/authorizationServer";
|
||||
import { Representation } from "./../../../../../models/representation";
|
||||
import { RouteHelper } from "../../../../../routing/routeHelper";
|
||||
import { Utils } from "../../../../../utils";
|
||||
import { TypeOfApi } from "../../../../../constants";
|
||||
import {
|
||||
TypeDefinition,
|
||||
TypeDefinitionProperty,
|
||||
TypeDefinitionPropertyTypeReference,
|
||||
TypeDefinitionPropertyTypeArrayOfReference,
|
||||
TypeDefinitionPropertyTypeArrayOfPrimitive
|
||||
} from "../../../../../models/typeDefinition";
|
||||
|
||||
|
||||
@RuntimeComponent({
|
||||
selector: "operation-details"
|
||||
})
|
||||
@Component({
|
||||
selector: "operation-details",
|
||||
template: template
|
||||
})
|
||||
export class OperationDetails {
|
||||
private readonly definitions: ko.ObservableArray<TypeDefinition>;
|
||||
public readonly selectedApiName: ko.Observable<string>;
|
||||
public readonly selectedOperationName: ko.Observable<string>;
|
||||
public readonly consoleIsOpen: ko.Observable<boolean>;
|
||||
public readonly api: ko.Observable<Api>;
|
||||
public readonly schemas: ko.ObservableArray<string>;
|
||||
public readonly tags: ko.ObservableArray<string>;
|
||||
public readonly operation: ko.Observable<Operation>;
|
||||
public readonly requestUrlSample: ko.Computed<string>;
|
||||
public readonly sampleHostname: ko.Observable<string>;
|
||||
public readonly hostnames: ko.Observable<string[]>;
|
||||
public readonly working: ko.Observable<boolean>;
|
||||
public readonly associatedAuthServer: ko.Observable<AuthorizationServer>;
|
||||
|
||||
constructor(
|
||||
private readonly apiService: ApiService,
|
||||
private readonly router: Router,
|
||||
private readonly routeHelper: RouteHelper
|
||||
) {
|
||||
this.working = ko.observable(false);
|
||||
this.sampleHostname = ko.observable();
|
||||
this.hostnames = ko.observable();
|
||||
this.associatedAuthServer = ko.observable();
|
||||
this.api = ko.observable();
|
||||
this.schemas = ko.observableArray([]);
|
||||
this.tags = ko.observableArray([]);
|
||||
this.operation = ko.observable();
|
||||
this.selectedApiName = ko.observable();
|
||||
this.selectedOperationName = ko.observable();
|
||||
this.consoleIsOpen = ko.observable();
|
||||
this.definitions = ko.observableArray<TypeDefinition>();
|
||||
this.defaultSchemaView = ko.observable("table");
|
||||
this.requestUrlSample = ko.computed(() => {
|
||||
if (!this.api() || !this.operation()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const api = this.api();
|
||||
const operation = this.operation();
|
||||
const hostname = this.sampleHostname();
|
||||
|
||||
let operationPath = api.versionedPath;
|
||||
|
||||
if (api.type !== TypeOfApi.soap) {
|
||||
operationPath += operation.displayUrlTemplate;
|
||||
}
|
||||
|
||||
return `https://${hostname}${Utils.ensureLeadingSlash(operationPath)}`;
|
||||
});
|
||||
}
|
||||
|
||||
@Param()
|
||||
public enableConsole: boolean;
|
||||
|
||||
@Param()
|
||||
public enableScrollTo: boolean;
|
||||
|
||||
@Param()
|
||||
public authorizationServers: AuthorizationServer[];
|
||||
|
||||
@Param()
|
||||
public defaultSchemaView: ko.Observable<string>;
|
||||
|
||||
@OnMounted()
|
||||
public async initialize(): Promise<void> {
|
||||
const apiName = this.routeHelper.getApiName();
|
||||
const operationName = this.routeHelper.getOperationName();
|
||||
|
||||
this.selectedApiName(apiName);
|
||||
this.selectedOperationName(operationName);
|
||||
this.router.addRouteChangeListener(this.onRouteChange.bind(this));
|
||||
|
||||
if (apiName) {
|
||||
await this.loadApi(apiName);
|
||||
}
|
||||
|
||||
if (operationName) {
|
||||
await this.loadOperation(apiName, operationName);
|
||||
}
|
||||
}
|
||||
|
||||
private async onRouteChange(): Promise<void> {
|
||||
const apiName = this.routeHelper.getApiName();
|
||||
const operationName = this.routeHelper.getOperationName();
|
||||
|
||||
if (apiName && apiName !== this.selectedApiName()) {
|
||||
this.selectedApiName(apiName);
|
||||
await this.loadApi(apiName);
|
||||
}
|
||||
|
||||
if (apiName === this.selectedApiName() && operationName === this.selectedOperationName()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!operationName) {
|
||||
this.selectedOperationName(null);
|
||||
this.operation(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (apiName && operationName) {
|
||||
this.selectedOperationName(operationName);
|
||||
await this.loadOperation(apiName, operationName);
|
||||
}
|
||||
}
|
||||
|
||||
public async loadApi(apiName: string): Promise<void> {
|
||||
if (!apiName) {
|
||||
return;
|
||||
}
|
||||
|
||||
const api = await this.apiService.getApi(`apis/${apiName}`);
|
||||
|
||||
if (!api) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.loadGatewayInfo(apiName);
|
||||
|
||||
this.api(api);
|
||||
|
||||
this.closeConsole();
|
||||
|
||||
const associatedServerId = api.authenticationSettings?.oAuth2?.authorizationServerId ||
|
||||
api.authenticationSettings?.openid?.openidProviderId;
|
||||
|
||||
let associatedAuthServer = null;
|
||||
|
||||
if (this.authorizationServers && associatedServerId) {
|
||||
associatedAuthServer = this.authorizationServers
|
||||
.find(x => x.name === associatedServerId);
|
||||
}
|
||||
|
||||
this.associatedAuthServer(associatedAuthServer);
|
||||
}
|
||||
|
||||
public async loadOperation(apiName: string, operationName: string): Promise<void> {
|
||||
this.working(true);
|
||||
|
||||
const operation = await this.apiService.getOperation(apiName, operationName);
|
||||
|
||||
if (operation) {
|
||||
await this.loadDefinitions(operation);
|
||||
this.operation(operation);
|
||||
}
|
||||
else {
|
||||
this.cleanSelection();
|
||||
}
|
||||
|
||||
const operationTags = await this.apiService.getOperationTags(`apis/${apiName}/operations/${operationName}`);
|
||||
this.tags(operationTags.map(tag => tag.name));
|
||||
|
||||
this.working(false);
|
||||
|
||||
if (this.enableScrollTo) {
|
||||
const headerElement = document.querySelector(".operation-header");
|
||||
headerElement && headerElement.scrollIntoView({ behavior: "smooth", block: "start", inline: "start" });
|
||||
}
|
||||
}
|
||||
|
||||
public async loadDefinitions(operation: Operation): Promise<void> {
|
||||
const schemaIds = [];
|
||||
const apiId = `apis/${this.selectedApiName()}/schemas`;
|
||||
|
||||
const representations = operation.responses
|
||||
.map(response => response.representations)
|
||||
.concat(operation.request.representations)
|
||||
.flat();
|
||||
|
||||
representations
|
||||
.map(representation => representation.schemaId)
|
||||
.filter(schemaId => !!schemaId)
|
||||
.forEach(schemaId => {
|
||||
if (!schemaIds.includes(schemaId)) {
|
||||
schemaIds.push(schemaId);
|
||||
}
|
||||
});
|
||||
|
||||
const typeNames = representations
|
||||
.filter(p => !!p.typeName)
|
||||
.map(p => p.typeName)
|
||||
.filter((item, pos, self) => self.indexOf(item) === pos);
|
||||
|
||||
const schemasPromises = schemaIds.map(schemaId => this.apiService.getApiSchema(`${apiId}/${schemaId}`));
|
||||
const schemas = await Promise.all(schemasPromises);
|
||||
const definitions = schemas.map(x => x.definitions).flat();
|
||||
|
||||
let lookupResult = [...typeNames];
|
||||
|
||||
while (lookupResult.length > 0) {
|
||||
const references = definitions.filter(definition => lookupResult.indexOf(definition.name) !== -1);
|
||||
|
||||
lookupResult = references.length === 0
|
||||
? []
|
||||
: this.lookupReferences(references, typeNames);
|
||||
|
||||
if (lookupResult.length > 0) {
|
||||
typeNames.push(...lookupResult);
|
||||
}
|
||||
}
|
||||
|
||||
this.definitions(definitions.filter(d => typeNames.indexOf(d.name) !== -1));
|
||||
}
|
||||
|
||||
private lookupReferences(definitions: TypeDefinition[], skipNames: string[]): string[] {
|
||||
const result = [];
|
||||
const objectDefinitions: TypeDefinitionProperty[] = definitions
|
||||
.map(definition => definition.properties)
|
||||
.filter(definition => !!definition)
|
||||
.flat();
|
||||
|
||||
objectDefinitions.forEach(definition => {
|
||||
if (definition.kind === "indexed") {
|
||||
result.push(definition.type["name"]);
|
||||
}
|
||||
|
||||
if ((definition.type instanceof TypeDefinitionPropertyTypeReference
|
||||
|| definition.type instanceof TypeDefinitionPropertyTypeArrayOfPrimitive
|
||||
|| definition.type instanceof TypeDefinitionPropertyTypeArrayOfReference)) {
|
||||
result.push(definition.type.name);
|
||||
}
|
||||
|
||||
if (definition.type instanceof TypeDefinitionPropertyTypeCombination) {
|
||||
result.push(...definition.type.combination.map(x => x["name"]));
|
||||
}
|
||||
});
|
||||
|
||||
return result.filter(x => !skipNames.includes(x));
|
||||
}
|
||||
|
||||
public async loadGatewayInfo(apiName: string): Promise<void> {
|
||||
const hostnames = await this.apiService.getApiHostnames(apiName);
|
||||
|
||||
if (hostnames.length === 0) {
|
||||
throw new Error(`Unable to fetch gateway hostnames.`);
|
||||
}
|
||||
|
||||
this.sampleHostname(hostnames[0]);
|
||||
this.hostnames(hostnames);
|
||||
}
|
||||
|
||||
private cleanSelection(): void {
|
||||
this.operation(null);
|
||||
this.selectedOperationName(null);
|
||||
this.closeConsole();
|
||||
}
|
||||
|
||||
public openConsole(): void {
|
||||
this.consoleIsOpen(true);
|
||||
}
|
||||
|
||||
public closeConsole(): void {
|
||||
this.consoleIsOpen(false);
|
||||
}
|
||||
|
||||
public getDefinitionForRepresentation(representation: Representation): TypeDefinition {
|
||||
let definition = this.definitions().find(x => x.name === representation.typeName);
|
||||
|
||||
if (!definition) {
|
||||
// Fallback for the case when type is referenced, but not defined in schema.
|
||||
return new TypeDefinition(representation.typeName, {});
|
||||
}
|
||||
|
||||
// Making copy to avoid overriding original properties.
|
||||
definition = Utils.clone(definition);
|
||||
|
||||
if (!definition.name) {
|
||||
definition.name = representation.typeName;
|
||||
}
|
||||
|
||||
if (representation.example) {
|
||||
definition.example = representation.example;
|
||||
definition.exampleFormat = representation.exampleFormat;
|
||||
}
|
||||
|
||||
return definition;
|
||||
}
|
||||
|
||||
public getDefinitionReferenceUrl(definition: TypeDefinition): string {
|
||||
const apiName = this.api().name;
|
||||
const operationName = this.operation().name;
|
||||
|
||||
return this.routeHelper.getDefinitionAnchor(apiName, operationName, definition.name);
|
||||
}
|
||||
|
||||
@OnDestroyed()
|
||||
public dispose(): void {
|
||||
this.router.removeRouteChangeListener(this.onRouteChange);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,136 @@
|
|||
using System;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Net.Http;
|
||||
using System.Web;
|
||||
|
||||
namespace CSHttpClientSample
|
||||
{
|
||||
static class Program
|
||||
{
|
||||
static void Main()
|
||||
{
|
||||
MakeRequest();
|
||||
Console.WriteLine("Hit ENTER to exit...");
|
||||
Console.ReadLine();
|
||||
}
|
||||
|
||||
static async void MakeRequest()
|
||||
{
|
||||
var client = new HttpClient();
|
||||
var queryString = HttpUtility.ParseQueryString(string.Empty);
|
||||
|
||||
{% if meaningfulHeaders.size > 0 -%}
|
||||
// Request headers
|
||||
{% for header in request.meaningfulHeaders -%}
|
||||
{% case header.Name -%}
|
||||
{% when "Accept"%}
|
||||
client.DefaultRequestHeaders.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("{{header.value}}"));
|
||||
{% when "Accept-Charset" -%}
|
||||
client.DefaultRequestHeaders.AcceptCharset.Add(StringWithQualityHeaderValue.Parse("{{header.value}}"));
|
||||
{% when "Accept-Encoding" -%}
|
||||
client.DefaultRequestHeaders.AcceptEncoding.Add(StringWithQualityHeaderValue.Parse("{{header.value}}"));
|
||||
{% when "Accept-Language" -%}
|
||||
client.DefaultRequestHeaders.AcceptLanguage.Add(StringWithQualityHeaderValue.Parse("{{header.value}}"));
|
||||
{% when "Cache-Control" -%}
|
||||
client.DefaultRequestHeaders.CacheControl = CacheControlHeaderValue.Parse("{{header.value}}");
|
||||
{% when "Connection" -%}
|
||||
client.DefaultRequestHeaders.Connection.Add("{{header.value}}");
|
||||
{% when "Date" -%}
|
||||
client.DefaultRequestHeaders.Date = DateTimeOffset.Parse("{{header.value}}");
|
||||
{% when "Expect" -%}
|
||||
client.DefaultRequestHeaders.Expect.Add(NameValueWithParametersHeaderValue.Parse("{{header.value}}"));
|
||||
{% when "If-Match" -%}
|
||||
client.DefaultRequestHeaders.IfMatch.Add(EntityTagHeaderValue.Parse("{{header.value}}"));
|
||||
{% when "If-Modified-Since" -%}
|
||||
client.DefaultRequestHeaders.IfModifiedSince = DateTimeOffset.Parse("{{header.value}}");
|
||||
{% when "If-None-Match" -%}
|
||||
client.DefaultRequestHeaders.IfNoneMatch.Add(EntityTagHeaderValue.Parse("{{header.value}}"));
|
||||
{% when "If-Range" -%}
|
||||
client.DefaultRequestHeaders.IfRange = RangeConditionHeaderValue.Parse("{{header.value}}");
|
||||
{% when "If-Unmodified-Since" -%}
|
||||
client.DefaultRequestHeaders.IfUnmodifiedSince = DateTimeOffset.Parse("{{header.value}}");
|
||||
{% when "Max-Forwards" -%}
|
||||
client.DefaultRequestHeaders.MaxForwards = int.Parse("{{header.value}}");
|
||||
{% when "Pragma" -%}
|
||||
client.DefaultRequestHeaders.Pragma.Add(NameValueHeaderValue.Parse("{{header.value}}"));
|
||||
{% when "Range" -%}
|
||||
client.DefaultRequestHeaders.Range = RangeHeaderValue.Parse("{{header.value}}");
|
||||
{% when "Referer" -%}
|
||||
client.DefaultRequestHeaders.Referrer = new Uri("{{header.value}}");
|
||||
{% when "TE" -%}
|
||||
client.DefaultRequestHeaders.TE.Add(TransferCodingWithQualityHeaderValue.Parse("{{header.value}}"));
|
||||
{% when "Transfer-Encoding" -%}
|
||||
client.DefaultRequestHeaders.TransferEncoding.Add(TransferCodingHeaderValue.Parse("{{header.value}}"));
|
||||
{% when "Upgrade" -%}
|
||||
client.DefaultRequestHeaders.Upgrade.Add(ProductHeaderValue.Parse("{{header.value}}"));
|
||||
{% when "User-Agent" -%}
|
||||
client.DefaultRequestHeaders.UserAgent.Add(ProductInfoHeaderValue.Parse("{{header.value}}"));
|
||||
{% when "Via" -%}
|
||||
client.DefaultRequestHeaders.Via.Add(ViaHeaderValue.Parse("{{header.value}}"));
|
||||
{% when "Warning" -%}
|
||||
client.DefaultRequestHeaders.Warning.Add(WarningHeaderValue.Parse("{{header.value}}"));
|
||||
{% when "Content-Type" -%}
|
||||
{% else -%}
|
||||
client.DefaultRequestHeaders.Add("{{header.Name}}", "{{header.value}}");
|
||||
{% endcase -%}
|
||||
{% endfor -%}
|
||||
{% endif -%}
|
||||
|
||||
{% if request.parameters.size > 0 -%}
|
||||
// Request parameters
|
||||
{% for parameter in request.parameters -%}
|
||||
queryString["{{parameter.Name}}"] = "{{parameter.Value}}";
|
||||
{% endfor -%}
|
||||
{% endif -%}
|
||||
var uri = "{{requestUrl}}{% if path contains '?' %}&{% else %}?{% endif %}" + queryString;
|
||||
|
||||
{% case method -%}
|
||||
|
||||
{% when "POST" -%}
|
||||
HttpResponseMessage response;
|
||||
|
||||
// Request body
|
||||
byte[] byteData = Encoding.UTF8.GetBytes("{{ body | replace:'"','\"'}}");
|
||||
|
||||
using (var content = new ByteArrayContent(byteData))
|
||||
{
|
||||
{% if body -%}
|
||||
content.Headers.ContentType = new MediaTypeHeaderValue("< your content type, i.e. application/json >");
|
||||
{% endif -%}
|
||||
response = await client.PostAsync(uri, content);
|
||||
}
|
||||
|
||||
{% when "GET" -%}
|
||||
var response = await client.GetAsync(uri);
|
||||
{% when "DELETE" -%}
|
||||
var response = await client.DeleteAsync(uri);
|
||||
{% when "PUT" -%}
|
||||
HttpResponseMessage response;
|
||||
|
||||
// Request body
|
||||
byte[] byteData = Encoding.UTF8.GetBytes("{{ body | replace:'"','\"'}}");
|
||||
|
||||
using (var content = new ByteArrayContent(byteData))
|
||||
{
|
||||
{% if body -%}
|
||||
content.Headers.ContentType = new MediaTypeHeaderValue("< your content type, i.e. application/json >");
|
||||
{% endif -%}
|
||||
response = await client.PutAsync(uri, content);
|
||||
}
|
||||
{% when "HEAD" -%}
|
||||
var response = await client.SendAsync(new HttpRequestMessage(HttpMethod.Head, uri));
|
||||
{% when "OPTIONS" -%}
|
||||
var response = await client.SendAsync(new HttpRequestMessage(HttpMethod.Options, uri));
|
||||
{% when "TRACE" -%}
|
||||
var response = await client.SendAsync(new HttpRequestMessage(HttpMethod.Trace, uri));
|
||||
|
||||
if (response.Content != null)
|
||||
{
|
||||
var responseString = await response.Content.ReadAsStringAsync();
|
||||
Console.WriteLine(responseString);
|
||||
}
|
||||
{% endcase -%}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
curl -v -X {{method}} "{{requestUrl}}"
|
||||
{% for header in request.meaningfulHeaders -%}
|
||||
-H "{{ header.name }}: {{ header.value }}"
|
||||
{% endfor -%}
|
||||
{% if request.body != blank and request.bodyFormat == "raw" -%}
|
||||
--data-raw '{{ request.body }}'
|
||||
{% endif -%}
|
||||
{% if request.binary != blank and request.bodyFormat == "binary" -%}
|
||||
--data-binary "@path/to/{{request.binary.name}}"
|
||||
{% endif -%}
|
|
@ -0,0 +1,11 @@
|
|||
{{method}} {{requestUrl}} HTTP/1.1
|
||||
|
||||
{% for header in request.meaningfulHeaders -%}
|
||||
{{ header.name }}: {{ header.value }}
|
||||
{% endfor %}
|
||||
{% if request.body != blank and request.bodyFormat == "raw" -%}
|
||||
{{ request.body }}
|
||||
{% endif -%}
|
||||
{% if request.binary != blank and request.bodyFormat == "binary" -%}
|
||||
[ {{ request.binary.name }} ]
|
||||
{% endif -%}
|
|
@ -0,0 +1,52 @@
|
|||
// // This sample uses the Apache HTTP client from HTTP Components (http://hc.apache.org/httpcomponents-client-ga/)
|
||||
import java.net.URI;
|
||||
import org.apache.http.HttpEntity;
|
||||
import org.apache.http.HttpResponse;
|
||||
import org.apache.http.client.HttpClient;
|
||||
import org.apache.http.client.methods.HttpGet;
|
||||
import org.apache.http.client.utils.URIBuilder;
|
||||
import org.apache.http.impl.client.HttpClients;
|
||||
import org.apache.http.util.EntityUtils;
|
||||
|
||||
public class JavaSample
|
||||
{
|
||||
public static void main(String[] args)
|
||||
{
|
||||
HttpClient httpclient = HttpClients.createDefault();
|
||||
|
||||
try
|
||||
{
|
||||
URIBuilder builder = new URIBuilder("{{requestUrl}}");
|
||||
|
||||
{% if request.parameters.size > 0 -%}
|
||||
{% for parameter in request.parameters -%}
|
||||
builder.setParameter("{{parameter.name}}", "{{parameter.value}}");
|
||||
{% endfor -%}
|
||||
{% endif -%}
|
||||
|
||||
URI uri = builder.build();
|
||||
Http{{ method | downcase | capitalize }} request = new Http{{ method | downcase | capitalize }}(uri);
|
||||
{% for header in request.meaningfulHeaders -%}
|
||||
request.setHeader("{{header.name}}", "{{header.value}}");
|
||||
{% endfor %}
|
||||
|
||||
{% if body -%}
|
||||
// Request body
|
||||
StringEntity reqEntity = new StringEntity("{{ body | replace:'"','\"' }}");
|
||||
request.setEntity(reqEntity);
|
||||
{% endif -%}
|
||||
|
||||
HttpResponse response = httpclient.execute(request);
|
||||
HttpEntity entity = response.getEntity();
|
||||
|
||||
if (entity != null)
|
||||
{
|
||||
System.out.println(EntityUtils.toString(entity));
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
System.out.println(e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
$.ajax({
|
||||
type: "{{method}}",
|
||||
url: "{{requestUrl}}",
|
||||
|
||||
{% if request.meaningfulHeaders.size > 0 -%}
|
||||
// Request headers
|
||||
beforeSend: function(xhrObj) {
|
||||
{% for header in request.meaningfulHeaders -%}
|
||||
xhrObj.setRequestHeader("{{header.name}}", "{{header.value}}");
|
||||
{% endfor -%}
|
||||
},
|
||||
{% endif -%}
|
||||
{% if request.body -%}
|
||||
|
||||
// Request body
|
||||
data: "{{ request.body | replace:'"','\"' }}",
|
||||
{% endif -%}
|
||||
})
|
||||
.done(function (data) {
|
||||
alert("success");
|
||||
})
|
||||
.fail(function () {
|
||||
alert("error");
|
||||
});
|
|
@ -0,0 +1,68 @@
|
|||
#import <Foundation/Foundation.h>
|
||||
|
||||
int main(int argc, const char * argv[])
|
||||
{
|
||||
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
|
||||
|
||||
NSString* path = @"{{requestUrl}}";
|
||||
NSArray* array = @[
|
||||
// Request parameters
|
||||
@"entities=true",
|
||||
{% if request.parameters.size > 0 -%}
|
||||
{% for parameter in request.parameters -%}
|
||||
@"{{parameter.name}}={{parameter.value}}",
|
||||
{% endfor -%}
|
||||
{% endif -%}
|
||||
];
|
||||
|
||||
NSString* string = [array componentsJoinedByString:@"&"];
|
||||
path = [path stringByAppendingFormat:@"?%@", string];
|
||||
|
||||
NSLog(@"%@", path);
|
||||
|
||||
NSMutableURLRequest* _request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:path]];
|
||||
[_request setHTTPMethod:@"{{method}}"];
|
||||
{% if meaningfulHeaders.size > 0 -%}
|
||||
// Request headers
|
||||
{% for header in request.meaningfulHeaders -%}
|
||||
[_request setValue:@"{{header.value}}" forHTTPHeaderField:@"{{header.name}}"];
|
||||
{% endfor -%}
|
||||
{% endif -%}
|
||||
{% if body -%}
|
||||
// Request body
|
||||
[_request setHTTPBody:[@"{{ body | replace:'"','\"' }}" dataUsingEncoding:NSUTF8StringEncoding]];
|
||||
{% endif -%}
|
||||
|
||||
NSURLResponse *response = nil;
|
||||
NSError *error = nil;
|
||||
NSData* _connectionData = [NSURLConnection sendSynchronousRequest:_request returningResponse:&response error:&error];
|
||||
|
||||
if (nil != error)
|
||||
{
|
||||
NSLog(@"Error: %@", error);
|
||||
}
|
||||
else
|
||||
{
|
||||
NSError* error = nil;
|
||||
NSMutableDictionary* json = nil;
|
||||
NSString* dataString = [[NSString alloc] initWithData:_connectionData encoding:NSUTF8StringEncoding];
|
||||
NSLog(@"%@", dataString);
|
||||
|
||||
if (nil != _connectionData)
|
||||
{
|
||||
json = [NSJSONSerialization JSONObjectWithData:_connectionData options:NSJSONReadingMutableContainers error:&error];
|
||||
}
|
||||
|
||||
if (error || !json)
|
||||
{
|
||||
NSLog(@"Could not parse loaded json with error:%@", error);
|
||||
}
|
||||
|
||||
NSLog(@"%@", json);
|
||||
_connectionData = nil;
|
||||
}
|
||||
|
||||
[pool drain];
|
||||
|
||||
return 0;
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
// This sample uses the Apache HTTP client from HTTP Components (http://hc.apache.org/httpcomponents-client-ga/)
|
||||
require_once 'HTTP/Request2.php';
|
||||
|
||||
$request = new Http_Request2('{{requestUrl}}');
|
||||
$url = $request->getUrl();
|
||||
|
||||
{% if meaningfulHeaders.size > 0 -%}
|
||||
$headers = array(
|
||||
// Request headers
|
||||
{% for header in request.meaningfulHeaders -%}
|
||||
'{{header.name}}' => '{{header.value}}',
|
||||
{% endfor -%}
|
||||
);
|
||||
|
||||
$request->setHeader($headers);
|
||||
{% endif -%}
|
||||
|
||||
{% if request.parameters.size > 0 -%}
|
||||
$parameters = array(
|
||||
// Request parameters
|
||||
{% for parameter in request.parameters -%}
|
||||
'{{parameter.name}}' => '{{parameter.value}}',
|
||||
{% endfor -%}
|
||||
);
|
||||
|
||||
$url->setQueryVariables($parameters);
|
||||
{% endif -%}
|
||||
|
||||
$request->setMethod(HTTP_Request2::METHOD_{{method}});
|
||||
|
||||
{% if body -%}
|
||||
// Request body
|
||||
$request->setBody("{{ body | replace:'"','\"' }}");
|
||||
{% endif -%}
|
||||
|
||||
try
|
||||
{
|
||||
$response = $request->send();
|
||||
echo $response->getBody();
|
||||
}
|
||||
catch (HttpException $ex)
|
||||
{
|
||||
echo $ex;
|
||||
}
|
||||
|
||||
?>
|
|
@ -0,0 +1,75 @@
|
|||
########### Python 2.7 #############
|
||||
import httplib, urllib, base64
|
||||
|
||||
headers = {
|
||||
{% if request.meaningfulHeaders.size > 0 -%}
|
||||
# Request headers
|
||||
{% for header in request.meaningfulHeaders -%}
|
||||
'{{header.name}}': '{{header.value}}',
|
||||
{% endfor -%}
|
||||
{% endif -%}
|
||||
}
|
||||
|
||||
params = urllib.urlencode({
|
||||
{% if request.parameters.size > 0 -%}
|
||||
# Request parameters
|
||||
{% for parameter in request.parameters -%}
|
||||
'{{parameter.name}}': '{{parameter.value}}',
|
||||
{% endfor -%}
|
||||
{% endif -%}
|
||||
})
|
||||
|
||||
try:
|
||||
{% case scheme -%}
|
||||
{% when "http" -%}
|
||||
conn = httplib.HTTPConnection('{{host}}')
|
||||
{% when "https" -%}
|
||||
conn = httplib.HTTPSConnection('{{host}}')
|
||||
{% endcase -%}
|
||||
conn.request("{{method}}", "{{path}}{% if path contains '?' %}&{% else %}?{% endif %}%s" % params{% if body %}, "{{ body | replace:'"','\"' }}"{% endif %}, headers)
|
||||
response = conn.getresponse()
|
||||
data = response.read()
|
||||
print(data)
|
||||
conn.close()
|
||||
except Exception as e:
|
||||
print("[Errno {0}] {1}".format(e.errno, e.strerror))
|
||||
|
||||
####################################
|
||||
|
||||
########### Python 3.2 #############
|
||||
import http.client, urllib.request, urllib.parse, urllib.error, base64
|
||||
|
||||
headers = {
|
||||
{% if request.meaningfulHeaders.size > 0 -%}
|
||||
# Request headers
|
||||
{% for header in request.meaningfulHeaders -%}
|
||||
'{{header.name}}': '{{header.value}}',
|
||||
{% endfor -%}
|
||||
{% endif -%}
|
||||
}
|
||||
|
||||
params = urllib.parse.urlencode({
|
||||
{% if parameters.size > 0 -%}
|
||||
# Request parameters
|
||||
{% for parameter in request.parameters -%}
|
||||
'{{parameter.name}}': '{{parameter.value}}',
|
||||
{% endfor -%}
|
||||
{% endif -%}
|
||||
})
|
||||
|
||||
try:
|
||||
{% case scheme -%}
|
||||
{% when "http" -%}
|
||||
conn = http.client.HTTPConnection('{{host}}')
|
||||
{% when "https" -%}
|
||||
conn = http.client.HTTPSConnection('{{host}}')
|
||||
{% endcase -%}
|
||||
conn.request("{{method}}", "{{path}}{% if path contains '?' %}&{% else %}?{% endif %}%s" % params{% if body %}, "{{ body | replace:'"','\"' }}"{% endif %}, headers)
|
||||
response = conn.getresponse()
|
||||
data = response.read()
|
||||
print(data)
|
||||
conn.close()
|
||||
except Exception as e:
|
||||
print("[Errno {0}] {1}".format(e.errno, e.strerror))
|
||||
|
||||
####################################
|
|
@ -0,0 +1,35 @@
|
|||
require 'net/http'
|
||||
|
||||
uri = URI('{{requestUrl}}')
|
||||
|
||||
{% if parameters.size > 0 -%}
|
||||
query = URI.encode_www_form({
|
||||
# Request parameters
|
||||
{% for parameter in request.parameters -%}
|
||||
'{{parameter.name}}' => '{{parameter.value}}'{% unless forloop.last %},{% endunless %}
|
||||
{% endfor -%}
|
||||
})
|
||||
if query.length > 0
|
||||
if uri.query && uri.query.length > 0
|
||||
uri.query += '&' + query
|
||||
else
|
||||
uri.query = query
|
||||
end
|
||||
end
|
||||
{% endif -%}
|
||||
|
||||
request = Net::HTTP::{{ method | downcase | capitalize }}.new(uri.request_uri)
|
||||
# Request headers
|
||||
{% for header in request.meaningfulHeaders -%}
|
||||
request['{{header.name}}'] = '{{header.value}}'
|
||||
{% endfor -%}
|
||||
{% if body -%}
|
||||
# Request body
|
||||
request.body = "{{ body | replace:'"','\"' }}"
|
||||
{% endif -%}
|
||||
|
||||
response = Net::HTTP.start(uri.host, uri.port, :use_ssl => uri.scheme == 'https') do |http|
|
||||
http.request(request)
|
||||
end
|
||||
|
||||
puts response.body
|
|
@ -0,0 +1,21 @@
|
|||
import * as curl from "./curl.liquid";
|
||||
import * as csharp from "./csharp.liquid";
|
||||
import * as http from "./http.liquid";
|
||||
import * as java from "./java.liquid";
|
||||
import * as javascript from "./javascript.liquid";
|
||||
import * as php from "./php.liquid";
|
||||
import * as objc from "./objc.liquid";
|
||||
import * as python from "./python.liquid";
|
||||
import * as ruby from "./ruby.liquid";
|
||||
|
||||
export const templates = {
|
||||
curl: curl.default,
|
||||
csharp: csharp.default,
|
||||
http: http.default,
|
||||
java: java.default,
|
||||
javascript: javascript.default,
|
||||
php: php.default,
|
||||
objc: objc.default,
|
||||
python: python.default,
|
||||
ruby: ruby.default
|
||||
};
|
|
@ -0,0 +1,21 @@
|
|||
<div role="table" class="table-preset table-preset-enum">
|
||||
<div class="d-contents" role="rowgroup">
|
||||
<div class="d-contents" role="row">
|
||||
<div class="table-preset-head text-truncate" role="columnheader">Type</div>
|
||||
<div class="table-preset-head text-truncate" role="columnheader">Values</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-contents" role="rowgroup">
|
||||
<div class="d-contents" role="row">
|
||||
<div role="cell" class="text-truncate"
|
||||
data-bind="markdown: definition.type.name, attr: { title: definition.type.name }"></div>
|
||||
|
||||
<div role="cell">
|
||||
<!-- ko foreach: { data: definition.enum, as: 'value' } -->
|
||||
<!-- ko if: $index() > 0 -->,
|
||||
<!-- /ko -->
|
||||
<code data-bind="text: value"></code><!-- /ko -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,27 @@
|
|||
<div class="scrollable-x">
|
||||
<div class="table" role="table">
|
||||
<div class="table-head" role="rowgroup">
|
||||
<div class="table-row" role="row">
|
||||
<div role="cell" class="col-4">Name</div>
|
||||
<div role="cell" class="col-2">Type</div>
|
||||
<div role="cell" class="col-6">Description</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-body" role="rowgroup" data-bind="foreach: { data: definition.properties, as: 'property' }">
|
||||
<div class="table-row" role="row">
|
||||
<div role="cell" class="col-4 text-truncate">
|
||||
<code data-bind="text: property.name, attr: { title: property.name }"></code>
|
||||
</div>
|
||||
<div role="cell" class="col-2 text-truncate">
|
||||
<!-- ko if: property.type.displayAs === 'reference' -->
|
||||
<code><a data-bind="text: property.type.name, attr: { href: $component.getReferenceUrl(property.type.name), attr: { title: property.type.name } }"></a></code>
|
||||
<!-- /ko -->
|
||||
<!-- ko if: property.type.displayAs === 'primitive' -->
|
||||
<code data-bind="text: property.type.name, attr: { title: property.type.name }"></code>
|
||||
<!-- /ko -->
|
||||
</div>
|
||||
<div role="cell" class="col-6" data-bind="markdown: property.description"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,71 @@
|
|||
<!-- ko if: definition.properties && definition.properties.length > 0 -->
|
||||
|
||||
<div role="table" class="table-preset table-preset-schema">
|
||||
<div class="d-contents" role="rowgroup">
|
||||
<div class="d-contents" role="row">
|
||||
<div role="cell" class="table-preset-head text-truncate">Name</div>
|
||||
<div role="cell" class="table-preset-head text-truncate">Required</div>
|
||||
<div role="cell" class="table-preset-head text-truncate">Type</div>
|
||||
<div role="cell" class="table-preset-head">Description</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-contents" role="rowgroup">
|
||||
<!-- ko foreach: { data: definition.properties, as: 'property' } -->
|
||||
<div class="d-contents" role="row">
|
||||
<div role="cell" class="text-truncate" data-bind="text: property.name, attr: { title: property.name }">
|
||||
<span class="monospace" data-bind="text: property.name, attr: { title: property.name }"></span>
|
||||
</div>
|
||||
<div role="cell" data-bind="text: property.required"></div>
|
||||
<div role="cell" class="text-truncate">
|
||||
<!-- ko if: property.type.displayAs === 'primitive' -->
|
||||
<span class="monospace"
|
||||
data-bind="text: property.type.name, attr: { title: property.type.name }"></span>
|
||||
<!-- /ko -->
|
||||
|
||||
<!-- ko if: property.type.displayAs === 'arrayOfPrimitive' -->
|
||||
<span class="monospace"
|
||||
data-bind="text: property.type.name, attr: { title: property.type.name }"></span>[]
|
||||
<!-- /ko -->
|
||||
|
||||
<!-- ko if: property.type.displayAs === 'reference' -->
|
||||
<a class="monospace"
|
||||
data-bind="text: property.type.name, attr: { href: $component.getReferenceUrl(property.type.name), title: property.type.name }"></a>
|
||||
<!-- /ko -->
|
||||
|
||||
<!-- ko if: property.type.displayAs === 'arrayOfReference' -->
|
||||
<a class="monospace"
|
||||
data-bind="text: property.type.name, attr: { href: $component.getReferenceUrl(property.type.name), title: property.type.name }"></a>[]
|
||||
<!-- /ko -->
|
||||
|
||||
<!-- ko if: property.type.displayAs === 'combination' -->
|
||||
<div>
|
||||
<span data-bind="text: property.type.combinationType"></span>:
|
||||
</div>
|
||||
<!-- ko foreach: { data: property.type.combination, as: 'item' } -->
|
||||
<!-- ko if: $index() > 0 -->,
|
||||
<!-- /ko -->
|
||||
<!-- ko if: item.displayAs === 'reference' -->
|
||||
<a class="monospace"
|
||||
data-bind="text: item.name, attr: { href: $component.getReferenceUrl(item.name), title: item.name }"></a>
|
||||
<!-- /ko -->
|
||||
|
||||
<!-- ko if: item.displayAs === 'primitive' -->
|
||||
<span class="monospace" data-bind="text: item, attr: { title: item.name }"></span>
|
||||
<!-- /ko -->
|
||||
|
||||
<!-- /ko -->
|
||||
|
||||
<!-- /ko -->
|
||||
</div>
|
||||
<div role="cell" data-bind="markdown: property.description"></div>
|
||||
</div>
|
||||
<!-- /ko -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- /ko -->
|
||||
|
||||
<!-- ko if: $component.example -->
|
||||
<h5>Example</h5>
|
||||
<code-sample params="{ content: $component.example, language: $component.exampleLanguage }"></code-sample>
|
||||
<!-- /ko -->
|
|
@ -0,0 +1,48 @@
|
|||
<!-- ko if: $component.anchor -->
|
||||
<h4 class="text-truncate flex-grow"
|
||||
data-bind="text: $component.name, attr: { id: $component.getReferenceId(definition) }">
|
||||
</h4>
|
||||
<!-- /ko -->
|
||||
<!-- ko ifnot: $component.anchor -->
|
||||
<h4 class="text-truncate flex-grow" data-bind="text: $component.name"></h4>
|
||||
<!-- /ko -->
|
||||
|
||||
<!-- ko if: $component.description -->
|
||||
<p data-bind="markdown: $component.description"></p>
|
||||
<!-- /ko -->
|
||||
|
||||
<ul class="nav nav-pills" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<a href="#" role="tab" class="nav-link active" title="Table view" data-bind="click: $component.switchToTable, css: { active: $component.schemaView() === 'table' }">
|
||||
<i class="icon-emb icon-emb-list"></i>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a href="#" role="tab" class="nav-link" title="Raw schema view" data-bind="click: $component.switchToRaw, css: { active: $component.schemaView() === 'raw' }">
|
||||
<i class="icon-emb icon-emb-brackets"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- ko if: schemaView() == 'raw' -->
|
||||
<code-sample params="{ content: $component.schemaObject, language: 'json' }"></code-sample>
|
||||
<!-- /ko -->
|
||||
|
||||
<!-- ko if: schemaView() == 'table' -->
|
||||
|
||||
<!-- ko if: $component.kind() === 'object' -->
|
||||
<!-- ko template: { name: 'typeDefinitionObject', data: $component } -->
|
||||
<!-- /ko -->
|
||||
<!-- /ko -->
|
||||
|
||||
<!-- ko if: $component.kind() === 'enum' -->
|
||||
<!-- ko template: { name: 'typeDefinitionEnum', data: $component } -->
|
||||
<!-- /ko -->
|
||||
<!-- /ko -->
|
||||
|
||||
<!-- ko if: $component.kind() === 'indexer' -->
|
||||
<!-- ko template: { name: 'typeDefinitionIndexer', data: $component } -->
|
||||
<!-- /ko -->
|
||||
<!-- /ko -->
|
||||
|
||||
<!-- /ko -->
|
|
@ -0,0 +1,83 @@
|
|||
import * as ko from "knockout";
|
||||
import template from "./type-definition.html";
|
||||
import typeDefinitionEnum from "./type-definition-enum.html";
|
||||
import typeDefinitionIndexer from "./type-definition-indexer.html";
|
||||
import typeDefinitionObject from "./type-definition-object.html";
|
||||
import { Component, Param, OnMounted } from "@paperbits/common/ko/decorators";
|
||||
import { TypeDefinition } from "../../../../../models/typeDefinition";
|
||||
import { RouteHelper } from "../../../../../routing/routeHelper";
|
||||
|
||||
@Component({
|
||||
selector: "type-definition",
|
||||
template: template,
|
||||
childTemplates: {
|
||||
typeDefinitionEnum: typeDefinitionEnum,
|
||||
typeDefinitionIndexer: typeDefinitionIndexer,
|
||||
typeDefinitionObject: typeDefinitionObject
|
||||
}
|
||||
})
|
||||
export class TypeDefinitionViewModel {
|
||||
public readonly name: ko.Observable<string>;
|
||||
public readonly description: ko.Observable<string>;
|
||||
public readonly kind: ko.Observable<string>;
|
||||
public readonly example: ko.Observable<string>;
|
||||
public readonly exampleLanguage: ko.Observable<string>;
|
||||
public readonly schemaObject: ko.Observable<string>;
|
||||
public readonly schemaView: ko.Observable<string>;
|
||||
|
||||
constructor(private readonly routeHelper: RouteHelper) {
|
||||
this.name = ko.observable();
|
||||
this.schemaObject = ko.observable();
|
||||
this.description = ko.observable();
|
||||
this.kind = ko.observable();
|
||||
this.example = ko.observable();
|
||||
this.exampleLanguage = ko.observable();
|
||||
this.schemaView = ko.observable();
|
||||
this.defaultSchemaView = ko.observable();
|
||||
}
|
||||
|
||||
@Param()
|
||||
public definition: TypeDefinition;
|
||||
|
||||
@Param()
|
||||
public apiName: string;
|
||||
|
||||
@Param()
|
||||
public operationName: string;
|
||||
|
||||
@Param()
|
||||
public anchor: string;
|
||||
|
||||
@Param()
|
||||
public defaultSchemaView: ko.Observable<string>;
|
||||
|
||||
@OnMounted()
|
||||
public initialize(): void {
|
||||
this.schemaView(this.defaultSchemaView() || "table");
|
||||
this.schemaObject(JSON.stringify(this.definition.schemaObject, null, 4));
|
||||
this.name(this.definition.name);
|
||||
this.description(this.definition.description);
|
||||
this.kind(this.definition.kind);
|
||||
|
||||
if (this.definition.example) {
|
||||
this.exampleLanguage(this.definition.exampleFormat);
|
||||
this.example(this.definition.example);
|
||||
}
|
||||
}
|
||||
|
||||
public getReferenceId(definition: TypeDefinition): string {
|
||||
return this.routeHelper.getDefinitionReferenceId(this.apiName, this.operationName, definition.name);
|
||||
}
|
||||
|
||||
public getReferenceUrl(typeName: string): string {
|
||||
return this.routeHelper.getDefinitionAnchor(this.apiName, this.operationName, typeName);
|
||||
}
|
||||
|
||||
public switchToTable(): void {
|
||||
this.schemaView("table");
|
||||
}
|
||||
|
||||
public switchToRaw(): void {
|
||||
this.schemaView("raw");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
import { IInjectorModule, IInjector } from "@paperbits/common/injection";
|
||||
import { OperationDetailsHandlers } from "./operationDetailsHandlers";
|
||||
import { OperationDetailsEditor } from "./ko/operationDetailsEditor";
|
||||
import { OperationDetailsModelBinder } from "./operationDetailsModelBinder";
|
||||
import { OperationDetailsViewModelBinder } from "./ko/operationDetailsViewModelBinder";
|
||||
|
||||
|
||||
export class OperationDetailsDesignModule implements IInjectorModule {
|
||||
public register(injector: IInjector): void {
|
||||
injector.bind("operationDetailsEditor", OperationDetailsEditor);
|
||||
injector.bindToCollection("widgetHandlers", OperationDetailsHandlers, "operationDetailsHandlers");
|
||||
injector.bindToCollection("modelBinders", OperationDetailsModelBinder);
|
||||
injector.bindToCollection("viewModelBinders", OperationDetailsViewModelBinder);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import { IInjectorModule, IInjector } from "@paperbits/common/injection";
|
||||
import { OperationDetailsModelBinder } from "./operationDetailsModelBinder";
|
||||
import { OperationDetailsViewModelBinder } from "./ko/operationDetailsViewModelBinder";
|
||||
|
||||
|
||||
export class OperationDetailsPublishModule implements IInjectorModule {
|
||||
public register(injector: IInjector): void {
|
||||
injector.bindToCollection("modelBinders", OperationDetailsModelBinder);
|
||||
injector.bindToCollection("viewModelBinders", OperationDetailsViewModelBinder);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
import { Contract } from "@paperbits/common";
|
||||
|
||||
export interface OperationDetailsContract extends Contract {
|
||||
/**
|
||||
* Indicates whether "Try" button should appear on the operation details widget.
|
||||
*/
|
||||
enableConsole?: boolean;
|
||||
|
||||
/**
|
||||
* Defines how schema gets presented in operation details by default, e.g. "table" or "raw".
|
||||
*/
|
||||
defaultSchemaView?: string;
|
||||
|
||||
/**
|
||||
* Indicates whether operation details should appear in the visible area (for example if API details is too long).
|
||||
*/
|
||||
enableScrollTo?: boolean;
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
import { IWidgetOrder, IWidgetHandler } from "@paperbits/common/editing";
|
||||
import { OperationDetailsModel } from "./operationDetailsModel";
|
||||
|
||||
export class OperationDetailsHandlers implements IWidgetHandler {
|
||||
public async getWidgetOrder(): Promise<IWidgetOrder> {
|
||||
const widgetOrder: IWidgetOrder = {
|
||||
name: "operationDetails",
|
||||
category: "Operations",
|
||||
displayName: "Operation: details",
|
||||
iconClass: "paperbits-cheque-3",
|
||||
requires: ["html"],
|
||||
createModel: async () => new OperationDetailsModel()
|
||||
};
|
||||
|
||||
return widgetOrder;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
import { AuthorizationServer } from "./../../../models/authorizationServer";
|
||||
|
||||
export class OperationDetailsModel {
|
||||
/**
|
||||
* Indicates whether "Try" button should appear on the operation details widget.
|
||||
*/
|
||||
public enableConsole?: boolean;
|
||||
|
||||
/**
|
||||
* Defines how schema gets presented in operation details by default, e.g. "table" or "raw".
|
||||
*/
|
||||
public defaultSchemaView?: string;
|
||||
|
||||
/**
|
||||
* External OAuth servers associated with API of this operation.
|
||||
*/
|
||||
public authorizationServers: AuthorizationServer[];
|
||||
|
||||
/**
|
||||
* Indicates whether operation details should appear in the visible area (for example if API details is too long).
|
||||
*/
|
||||
public enableScrollTo?: boolean;
|
||||
|
||||
constructor() {
|
||||
this.enableConsole = true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
import { OAuthService } from "./../../../services/oauthService";
|
||||
import { Contract } from "@paperbits/common";
|
||||
import { IModelBinder } from "@paperbits/common/editing";
|
||||
import { OperationDetailsModel } from "./operationDetailsModel";
|
||||
import { OperationDetailsContract } from "./operationDetailsContract";
|
||||
|
||||
export class OperationDetailsModelBinder implements IModelBinder<OperationDetailsModel> {
|
||||
constructor(private readonly oauthService: OAuthService) { }
|
||||
|
||||
public canHandleContract(contract: Contract): boolean {
|
||||
return contract.type === "operationDetails";
|
||||
}
|
||||
|
||||
public canHandleModel(model: Object): boolean {
|
||||
return model instanceof OperationDetailsModel;
|
||||
}
|
||||
|
||||
public async contractToModel(contract: OperationDetailsContract): Promise<OperationDetailsModel> {
|
||||
const model = new OperationDetailsModel();
|
||||
model.enableConsole = contract.enableConsole === true || contract.enableConsole === undefined;
|
||||
model.enableScrollTo = contract.enableScrollTo !== undefined && contract.enableScrollTo === true;
|
||||
model.defaultSchemaView = contract.defaultSchemaView || "table";
|
||||
model.authorizationServers = await this.oauthService.getOAuthServers();
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
public modelToContract(model: OperationDetailsModel): Contract {
|
||||
const contract: OperationDetailsContract = {
|
||||
type: "operationDetails",
|
||||
enableConsole: model.enableConsole,
|
||||
enableScrollTo: model.enableScrollTo,
|
||||
defaultSchemaView: model.defaultSchemaView
|
||||
};
|
||||
|
||||
return contract;
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
<operation-list class="d-block" data-bind="attr: { params: runtimeConfig }"></operation-list>
|
|
@ -0,0 +1,11 @@
|
|||
import { IInjectorModule, IInjector } from "@paperbits/common/injection";
|
||||
import { OperationListModelBinder } from "../operationListModelBinder";
|
||||
import { OperationListViewModelBinder } from "./operationListViewModelBinder";
|
||||
|
||||
|
||||
export class OperationListModule implements IInjectorModule {
|
||||
public register(injector: IInjector): void {
|
||||
injector.bindToCollection("modelBinders", OperationListModelBinder);
|
||||
injector.bindToCollection("viewModelBinders", OperationListViewModelBinder);
|
||||
}
|
||||
}
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче