This commit is contained in:
Alexander Zaslonov 2021-03-12 12:04:28 -08:00
Коммит 0861ef70b9
376 изменённых файлов: 62256 добавлений и 0 удалений

12
.gitattributes поставляемый Normal file
Просмотреть файл

@ -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

26
.github/workflows/publish-on-checkin.yml поставляемый Normal file
Просмотреть файл

@ -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

5
.gitignore поставляемый Normal file
Просмотреть файл

@ -0,0 +1,5 @@
.history/
.vs/
.vscode/
dist/
node_modules/

21
LICENSE Normal file
Просмотреть файл

@ -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

58
README.md Normal file
Просмотреть файл

@ -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.

1631
data/block-snippets.json Normal file

Разница между файлами не показана из-за своего большого размера Загрузить разницу

5140
data/content.json Normal file

Разница между файлами не показана из-за своего большого размера Загрузить разницу

25
data/icon-fonts.json Normal file
Просмотреть файл

@ -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"
}
}
}
}
}
}

1628
data/style-snippets.json Normal file

Разница между файлами не показана из-за своего большого размера Загрузить разницу

30338
package-lock.json сгенерированный Normal file

Разница между файлами не показана из-за своего большого размера Загрузить разницу

104
package.json Normal file
Просмотреть файл

@ -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"
}
}

7
postcss.config.js Normal file
Просмотреть файл

@ -0,0 +1,7 @@
module.exports = {
plugins: [
require("autoprefixer")
],
sourceMap: true,
minimize: true
}

Двоичные данные
readme.gif Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 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>

79
src/components/app/app.ts Normal file
Просмотреть файл

@ -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);
}
}

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше