mirror of
https://github.com/Redocly/redoc.git
synced 2025-01-31 10:04:08 +03:00
feat: Add support for x-servers
This commit is contained in:
parent
dcb97a5b96
commit
fd49082db2
|
@ -79,6 +79,11 @@ securityDefinitions:
|
|||
type: apiKey
|
||||
name: api_key
|
||||
in: header
|
||||
x-servers:
|
||||
- url: //petstore.swagger.io/v2
|
||||
description: Default server
|
||||
- url: //petstore.swagger.io/sandbox
|
||||
description: Sandbox server
|
||||
paths:
|
||||
/pet:
|
||||
post:
|
||||
|
|
17
lib/components/EndpointLink/endpoint-link.html
Normal file
17
lib/components/EndpointLink/endpoint-link.html
Normal file
|
@ -0,0 +1,17 @@
|
|||
<div class="method-endpoint" (click)="handleClick()">
|
||||
<h5 class="http-verb" [ngClass]="verb">{{verb}}</h5>
|
||||
<span><!--
|
||||
--><span class="method-api-url-path">{{path}}</span><!--
|
||||
--></span>
|
||||
<svg class="expand-icon" xmlns="http://www.w3.org/2000/svg" version="1.1" x="0" y="0" viewBox="0 0 24 24" xml:space="preserve">
|
||||
<polygon fill="white" points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="servers-overlay" [@overlayExpand]="expanded ? 'expanded' : 'collapsed'">
|
||||
<div *ngFor="let server of servers" class="server-item">
|
||||
<div class="description" [innerHtml]="server.description | marked"></div>
|
||||
<div select-on-click class="url">
|
||||
<span class="method-api-url"> {{server.url}}</span>{{path}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
101
lib/components/EndpointLink/endpoint-link.scss
Normal file
101
lib/components/EndpointLink/endpoint-link.scss
Normal file
|
@ -0,0 +1,101 @@
|
|||
@import '../../shared/styles/variables';
|
||||
|
||||
:host {
|
||||
display: block;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.method-endpoint {
|
||||
padding: 10px 20px;
|
||||
border-radius: $border-radius*2;
|
||||
background-color: darken($black, 2%);
|
||||
display: block;
|
||||
font-weight: $light;
|
||||
white-space: nowrap;
|
||||
overflow-x: auto;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.method-endpoint > .method-params-subheader {
|
||||
padding-top: 1px;
|
||||
padding-bottom: 0;
|
||||
margin: 0;
|
||||
font-size: 12/14em;
|
||||
color: $black;
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
border-radius: $border-radius;
|
||||
}
|
||||
|
||||
.method-api-url {
|
||||
color: rgba($black, .8);
|
||||
&-path {
|
||||
font-family: $headers-font, $headers-font-family;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
color: #ffffff;
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.http-verb {
|
||||
color: $black;
|
||||
background: #ffffff;
|
||||
padding: 3px 10px;
|
||||
text-transform: uppercase;
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.servers-overlay {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
z-index: 100;
|
||||
background: $side-bar-bg-color;
|
||||
color: $black;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 4px 4px 6px rgba(0, 0, 0, 0.33);
|
||||
overflow: hidden;
|
||||
border-bottom-left-radius: $border-radius*2;
|
||||
border-bottom-right-radius: $border-radius*2;
|
||||
}
|
||||
|
||||
.server-item {
|
||||
padding: 10px;
|
||||
//margin-bottom: 10px;
|
||||
|
||||
& > .url {
|
||||
padding: 5px;
|
||||
border: 1px solid $border-color;
|
||||
background: $background-color;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.expand-icon {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
display: inline-block;
|
||||
float: right;
|
||||
margin-top: 2px;
|
||||
background: darken($black, 2%);
|
||||
transform: rotateZ(0);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
:host.expanded {
|
||||
> .method-endpoint {
|
||||
border-color: $side-bar-bg-color;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.expand-icon {
|
||||
transform: rotateZ(180deg);
|
||||
}
|
||||
}
|
79
lib/components/EndpointLink/endpoint-link.spec.ts
Normal file
79
lib/components/EndpointLink/endpoint-link.spec.ts
Normal file
|
@ -0,0 +1,79 @@
|
|||
'use strict';
|
||||
|
||||
import { Component } from '@angular/core';
|
||||
import {
|
||||
inject,
|
||||
async,
|
||||
TestBed
|
||||
} from '@angular/core/testing';
|
||||
|
||||
import { getChildDebugElement } from '../../../tests/helpers';
|
||||
|
||||
import { EndpointLink } from './endpoint-link';
|
||||
import { SpecManager } from '../../utils/spec-manager';
|
||||
|
||||
describe('Redoc components', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({ declarations: [ TestAppComponent ] });
|
||||
});
|
||||
describe('EndpointLink Component', () => {
|
||||
let builder;
|
||||
let component: EndpointLink;
|
||||
let specMgr: SpecManager;
|
||||
|
||||
beforeEach(async(inject([SpecManager], (_specMgr) => {
|
||||
specMgr = _specMgr;
|
||||
})));
|
||||
|
||||
beforeEach(() => {
|
||||
specMgr.apiUrl = 'http://test.com/v1';
|
||||
specMgr._schema = {
|
||||
info: {},
|
||||
host: 'petstore.swagger.io',
|
||||
baseName: '/v2',
|
||||
schemes: ['https', 'http'],
|
||||
'x-servers': [
|
||||
{
|
||||
url: '//test.com/v2'
|
||||
},
|
||||
{
|
||||
url: 'ws://test.com/v3',
|
||||
description: 'test'
|
||||
}
|
||||
]
|
||||
};
|
||||
specMgr.init();
|
||||
|
||||
component = new EndpointLink(specMgr);
|
||||
});
|
||||
|
||||
it('should replace // with appropriate protocol', () => {
|
||||
component.ngOnInit();
|
||||
component.servers[0].url.should.be.equal('https://test.com/v2');
|
||||
});
|
||||
|
||||
|
||||
it('should preserve other protocols', () => {
|
||||
component.ngOnInit();
|
||||
component.servers[1].url.should.be.equal('ws://test.com/v3');
|
||||
});
|
||||
|
||||
it('should fallback to host + basePath + schemas if no x-servers', () => {
|
||||
specMgr._schema['x-servers'] = null;
|
||||
specMgr.init();
|
||||
component.ngOnInit();
|
||||
component.servers.should.be.lengthOf(1);
|
||||
component.servers[0].url.should.be.equal('https://petstore.swagger.io');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
/** Test component that contains a Method. */
|
||||
@Component({
|
||||
selector: 'test-app',
|
||||
template:
|
||||
`<method pointer='#/paths/~1user~1{username}/put'></method>`
|
||||
})
|
||||
class TestAppComponent {
|
||||
}
|
69
lib/components/EndpointLink/endpoint-link.ts
Normal file
69
lib/components/EndpointLink/endpoint-link.ts
Normal file
|
@ -0,0 +1,69 @@
|
|||
'use strict';
|
||||
import { Component, ChangeDetectionStrategy, Input, OnInit, HostListener, HostBinding} from '@angular/core';
|
||||
import { BaseComponent, SpecManager } from '../base';
|
||||
import { trigger, state, animate, transition, style } from '@angular/core';
|
||||
|
||||
export interface ServerInfo {
|
||||
description: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'endpoint-link',
|
||||
styleUrls: ['./endpoint-link.css'],
|
||||
templateUrl: './endpoint-link.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
animations: [
|
||||
trigger('overlayExpand', [
|
||||
state('collapsed, void',
|
||||
style({ height: '0px' })),
|
||||
state('expanded',
|
||||
style({ height: '*' })),
|
||||
transition('collapsed <=> expanded', [
|
||||
animate('200ms ease')
|
||||
])
|
||||
])
|
||||
]
|
||||
})
|
||||
export class EndpointLink implements OnInit {
|
||||
@Input() path:string;
|
||||
@Input() verb:string;
|
||||
|
||||
apiUrl: string;
|
||||
servers: ServerInfo[];
|
||||
@HostBinding('class.expanded') expanded: boolean = false;
|
||||
|
||||
// @HostListener('click')
|
||||
handleClick() {
|
||||
this.expanded = !this.expanded;
|
||||
}
|
||||
|
||||
constructor(public specMgr:SpecManager) {
|
||||
this.expanded = false;
|
||||
}
|
||||
|
||||
init() {
|
||||
let servers:ServerInfo[] = this.specMgr.schema['x-servers'];
|
||||
if (servers) {
|
||||
this.servers = servers.map(({url, description}) => ({
|
||||
description,
|
||||
url: url.startsWith('//') ? `${this.specMgr.apiProtocol}:${url}` : url
|
||||
}));
|
||||
} else {
|
||||
this.servers = [
|
||||
{
|
||||
description: 'Server URL',
|
||||
url: this.getBaseUrl()
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
getBaseUrl():string {
|
||||
return this.specMgr.apiUrl;
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.init();
|
||||
}
|
||||
}
|
|
@ -16,12 +16,7 @@
|
|||
<div class="method-samples">
|
||||
<h4 class="method-params-subheader">Definition</h4>
|
||||
|
||||
<div class="method-endpoint">
|
||||
<h5 class="http-method" [ngClass]="method.httpMethod">{{method.httpMethod}}</h5>
|
||||
<span select-on-click><!--
|
||||
--><span class="method-api-url">{{method.apiUrl}}</span><span class="method-api-url-path">{{method.path}}</span><!--
|
||||
--></span>
|
||||
</div>
|
||||
<endpoint-link [verb]="method.verb" [path]="method.path"> </endpoint-link>
|
||||
|
||||
<div>
|
||||
<request-samples [pointer]="pointer" [schemaPointer]="method.bodyParam?._pointer">
|
||||
|
|
|
@ -18,45 +18,6 @@
|
|||
margin-bottom: calc(1em - 6px);
|
||||
}
|
||||
|
||||
.method-endpoint {
|
||||
//margin: 0 0 2px 0;
|
||||
padding: 10px 20px;
|
||||
border-radius: $border-radius*2;
|
||||
background-color: darken($black, 2%);
|
||||
display: block;
|
||||
font-weight: $light;
|
||||
white-space: nowrap;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.method-endpoint > .method-params-subheader {
|
||||
padding-top: 1px;
|
||||
padding-bottom: 0;
|
||||
margin: 0;
|
||||
font-size: 12/14em;
|
||||
color: $black;
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
border-radius: $border-radius;
|
||||
}
|
||||
|
||||
.method-api-url {
|
||||
color: rgba(#ffffff, .6);
|
||||
margin-left: 10px;
|
||||
margin-top: 2px;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
font-family: $headers-font, $headers-font-family;
|
||||
font-size: 0.929em;
|
||||
|
||||
&-path {
|
||||
font-family: $headers-font, $headers-font-family;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
.method-tags {
|
||||
margin-top: 20px;
|
||||
|
||||
|
@ -121,15 +82,6 @@
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
.http-method {
|
||||
color: $black;
|
||||
background: #ffffff;
|
||||
padding: 3px 10px;
|
||||
text-transform: uppercase;
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
[select-on-click] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ describe('Redoc components', () => {
|
|||
});
|
||||
describe('Method Component', () => {
|
||||
let builder;
|
||||
let component;
|
||||
let component: Method;
|
||||
let specMgr;
|
||||
|
||||
beforeEach(async(inject([SpecManager, LazyTasksService], (_specMgr, lazyTasks) => {
|
||||
|
@ -43,8 +43,7 @@ describe('Redoc components', () => {
|
|||
});
|
||||
|
||||
it('should init basic component data', () => {
|
||||
component.method.apiUrl.should.be.equal('http://petstore.swagger.io/v2');
|
||||
component.method.httpMethod.should.be.equal('put');
|
||||
component.method.verb.should.be.equal('put');
|
||||
component.method.path.should.be.equal('/user/{username}');
|
||||
});
|
||||
|
||||
|
|
|
@ -7,8 +7,7 @@ import { OptionsService } from '../../services/';
|
|||
|
||||
|
||||
interface MethodInfo {
|
||||
apiUrl: string;
|
||||
httpMethod: string;
|
||||
verb: string;
|
||||
path: string;
|
||||
info: {
|
||||
tags: string[];
|
||||
|
@ -45,7 +44,7 @@ export class Method extends BaseComponent implements OnInit {
|
|||
this.operationId = this.componentSchema.operationId;
|
||||
|
||||
this.method = {
|
||||
httpMethod: JsonPointer.baseName(this.pointer),
|
||||
verb: JsonPointer.baseName(this.pointer),
|
||||
path: JsonPointer.baseName(this.pointer, 2),
|
||||
info: {
|
||||
description: this.componentSchema.description,
|
||||
|
@ -53,7 +52,6 @@ export class Method extends BaseComponent implements OnInit {
|
|||
},
|
||||
bodyParam: this.findBodyParam(),
|
||||
summary: SchemaHelper.methodSummary(this.componentSchema),
|
||||
apiUrl: this.getBaseUrl(),
|
||||
anchor: this.buildAnchor(),
|
||||
externalDocs: this.componentSchema.externalDocs
|
||||
};
|
||||
|
@ -67,14 +65,6 @@ export class Method extends BaseComponent implements OnInit {
|
|||
}
|
||||
}
|
||||
|
||||
getBaseUrl():string {
|
||||
if (this.optionsService.options.hideHostname) {
|
||||
return this.specMgr.basePath;
|
||||
} else {
|
||||
return this.specMgr.apiUrl;
|
||||
}
|
||||
}
|
||||
|
||||
filterMainTags(tags) {
|
||||
var tagsMap = this.specMgr.getTagsMap();
|
||||
if (!tags) return [];
|
||||
|
|
|
@ -17,15 +17,16 @@ import { SecurityDefinitions } from './SecurityDefinitions/security-definitions'
|
|||
import { LoadingBar } from './LoadingBar/loading-bar';
|
||||
import { RedocSearch } from './Search/redoc-search';
|
||||
import { ExternalDocs } from './ExternalDocs/external-docs';
|
||||
import { EndpointLink } from './EndpointLink/endpoint-link';
|
||||
|
||||
import { Redoc } from './Redoc/redoc';
|
||||
|
||||
export const REDOC_DIRECTIVES = [
|
||||
ApiInfo, ApiLogo, JsonSchema, JsonSchemaLazy, ParamsList, RequestSamples, ResponsesList,
|
||||
ResponsesSamples, SchemaSample, SideMenu, MethodsList, Method, Warnings, Redoc, SecurityDefinitions,
|
||||
LoadingBar, SideMenuItems, RedocSearch, ExternalDocs
|
||||
LoadingBar, SideMenuItems, RedocSearch, ExternalDocs, EndpointLink
|
||||
];
|
||||
|
||||
export { ApiInfo, ApiLogo, JsonSchema, JsonSchemaLazy, ParamsList, RequestSamples, ResponsesList,
|
||||
ResponsesSamples, SchemaSample, SideMenu, MethodsList, Method, Warnings, Redoc, SecurityDefinitions,
|
||||
LoadingBar, SideMenuItems, RedocSearch, ExternalDocs }
|
||||
LoadingBar, SideMenuItems, RedocSearch, ExternalDocs, EndpointLink }
|
||||
|
|
|
@ -7,6 +7,7 @@ $green: #00aa13;
|
|||
$yellow: #f1c400;
|
||||
$red: #e53935;
|
||||
$background-color: #fff;
|
||||
$border-color: #ccc;
|
||||
|
||||
$em-size: 14px;
|
||||
|
||||
|
|
|
@ -24,10 +24,12 @@ export interface DescendantInfo {
|
|||
export class SpecManager {
|
||||
public _schema: any = {};
|
||||
public apiUrl: string;
|
||||
public apiProtocol: string;
|
||||
public swagger: string;
|
||||
public basePath: string;
|
||||
|
||||
public spec = new BehaviorSubject<any|null>(null);
|
||||
private _url: string;
|
||||
public _specUrl: string;
|
||||
private parser: any;
|
||||
|
||||
load(urlOrObject: string|Object) {
|
||||
|
@ -36,7 +38,7 @@ export class SpecManager {
|
|||
this.parser.bundle(urlOrObject, {http: {withCredentials: false}})
|
||||
.then(schema => {
|
||||
if (typeof urlOrObject === 'string') {
|
||||
this._url = urlOrObject;
|
||||
this._specUrl = urlOrObject;
|
||||
}
|
||||
this._schema = snapshot(schema);
|
||||
try {
|
||||
|
@ -54,7 +56,7 @@ export class SpecManager {
|
|||
|
||||
/* calculate common used values */
|
||||
init() {
|
||||
let urlParts = this._url ? urlParse(urlResolve(window.location.href, this._url)) : {};
|
||||
let urlParts = this._specUrl ? urlParse(urlResolve(window.location.href, this._specUrl)) : {};
|
||||
let schemes = this._schema.schemes;
|
||||
let protocol;
|
||||
if (!schemes || !schemes.length) {
|
||||
|
@ -70,6 +72,7 @@ export class SpecManager {
|
|||
let host = this._schema.host || urlParts.host;
|
||||
this.basePath = this._schema.basePath || '';
|
||||
this.apiUrl = protocol + '://' + host + this.basePath;
|
||||
this.apiProtocol = protocol;
|
||||
if (this.apiUrl.endsWith('/')) {
|
||||
this.apiUrl = this.apiUrl.substr(0, this.apiUrl.length - 1);
|
||||
}
|
||||
|
@ -79,6 +82,9 @@ export class SpecManager {
|
|||
|
||||
preprocess() {
|
||||
let mdRender = new MdRenderer();
|
||||
if (!this._schema.info) {
|
||||
throw 'Required field "info" is not specified at the spec top level';
|
||||
}
|
||||
if (!this._schema.info.description) this._schema.info.description = '';
|
||||
if (this._schema.securityDefinitions) {
|
||||
let SecurityDefinitions = require('../components/').SecurityDefinitions;
|
||||
|
@ -168,7 +174,7 @@ export class SpecManager {
|
|||
return tagsMap;
|
||||
}
|
||||
|
||||
findDerivedDefinitions(defPointer: string, schema): DescendantInfo[] {
|
||||
findDerivedDefinitions(defPointer: string, schema?: any): DescendantInfo[] {
|
||||
let definition = schema || this.byPointer(defPointer);
|
||||
if (!definition) throw new Error(`Can't load schema at ${defPointer}`);
|
||||
if (!definition.discriminator && !definition['x-extendedDiscriminator']) return [];
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
import { SpecManager } from '../../lib/utils/spec-manager';
|
||||
describe('Utils', () => {
|
||||
describe('Schema manager', () => {
|
||||
let specMgr;
|
||||
let specMgr: SpecManager;
|
||||
|
||||
beforeEach(() => {
|
||||
specMgr = new SpecManager();
|
||||
|
@ -51,21 +51,21 @@ describe('Utils', () => {
|
|||
|
||||
it('should substitute api scheme when spec schemes are undefined', () => {
|
||||
specMgr._schema.schemes = undefined;
|
||||
specMgr._url = 'https://petstore.swagger.io/v2';
|
||||
specMgr._specUrl = 'https://petstore.swagger.io/v2';
|
||||
specMgr.init();
|
||||
specMgr.apiUrl.should.be.equal('https://petstore.swagger.io/v2');
|
||||
});
|
||||
|
||||
it('should substitute api host when spec host is undefined', () => {
|
||||
specMgr._schema.host = undefined;
|
||||
specMgr._url = 'http://petstore.swagger.io/v2';
|
||||
specMgr._specUrl = 'http://petstore.swagger.io/v2';
|
||||
specMgr.init();
|
||||
specMgr.apiUrl.should.be.equal('http://petstore.swagger.io/v2');
|
||||
});
|
||||
|
||||
it('should use empty basePath when basePath is not present', () => {
|
||||
specMgr._schema.basePath = undefined;
|
||||
specMgr._url = 'https://petstore.swagger.io';
|
||||
specMgr._specUrl = 'https://petstore.swagger.io';
|
||||
specMgr.init();
|
||||
specMgr.basePath.should.be.equal('');
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue
Block a user