diff --git a/demo/swagger.yaml b/demo/swagger.yaml
index b3cae32d..f21ecef5 100644
--- a/demo/swagger.yaml
+++ b/demo/swagger.yaml
@@ -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:
diff --git a/lib/components/EndpointLink/endpoint-link.html b/lib/components/EndpointLink/endpoint-link.html
new file mode 100644
index 00000000..2763667b
--- /dev/null
+++ b/lib/components/EndpointLink/endpoint-link.html
@@ -0,0 +1,17 @@
+
+
{{verb}}
+
{{path}}
+
+
+
+
+
+
+ {{server.url}}{{path}}
+
+
+
diff --git a/lib/components/EndpointLink/endpoint-link.scss b/lib/components/EndpointLink/endpoint-link.scss
new file mode 100644
index 00000000..e7f59fe3
--- /dev/null
+++ b/lib/components/EndpointLink/endpoint-link.scss
@@ -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);
+ }
+}
diff --git a/lib/components/EndpointLink/endpoint-link.spec.ts b/lib/components/EndpointLink/endpoint-link.spec.ts
new file mode 100644
index 00000000..99664065
--- /dev/null
+++ b/lib/components/EndpointLink/endpoint-link.spec.ts
@@ -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:
+ ``
+})
+class TestAppComponent {
+}
diff --git a/lib/components/EndpointLink/endpoint-link.ts b/lib/components/EndpointLink/endpoint-link.ts
new file mode 100644
index 00000000..263e11af
--- /dev/null
+++ b/lib/components/EndpointLink/endpoint-link.ts
@@ -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();
+ }
+}
diff --git a/lib/components/Method/method.html b/lib/components/Method/method.html
index c031ca74..5bc116ce 100644
--- a/lib/components/Method/method.html
+++ b/lib/components/Method/method.html
@@ -16,12 +16,7 @@
-
-
{{method.httpMethod}}
- {{method.apiUrl}}{{method.path}}
-
+
diff --git a/lib/components/Method/method.scss b/lib/components/Method/method.scss
index 0306b61e..6194a628 100644
--- a/lib/components/Method/method.scss
+++ b/lib/components/Method/method.scss
@@ -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;
}
diff --git a/lib/components/Method/method.spec.ts b/lib/components/Method/method.spec.ts
index d754067c..0d4eb5fe 100644
--- a/lib/components/Method/method.spec.ts
+++ b/lib/components/Method/method.spec.ts
@@ -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}');
});
diff --git a/lib/components/Method/method.ts b/lib/components/Method/method.ts
index 852290bb..3da98f13 100644
--- a/lib/components/Method/method.ts
+++ b/lib/components/Method/method.ts
@@ -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 [];
diff --git a/lib/components/index.ts b/lib/components/index.ts
index f26d2f10..4ab87066 100644
--- a/lib/components/index.ts
+++ b/lib/components/index.ts
@@ -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 }
diff --git a/lib/shared/styles/_variables.scss b/lib/shared/styles/_variables.scss
index e31b8a65..62e5ba0e 100644
--- a/lib/shared/styles/_variables.scss
+++ b/lib/shared/styles/_variables.scss
@@ -7,6 +7,7 @@ $green: #00aa13;
$yellow: #f1c400;
$red: #e53935;
$background-color: #fff;
+$border-color: #ccc;
$em-size: 14px;
diff --git a/lib/utils/spec-manager.ts b/lib/utils/spec-manager.ts
index 516872c3..951a18c0 100644
--- a/lib/utils/spec-manager.ts
+++ b/lib/utils/spec-manager.ts
@@ -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(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 [];
diff --git a/tests/unit/SpecManager.spec.ts b/tests/unit/SpecManager.spec.ts
index d5488c35..f79e1409 100644
--- a/tests/unit/SpecManager.spec.ts
+++ b/tests/unit/SpecManager.spec.ts
@@ -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('');
});