diff --git a/docs/features/software-catalog/descriptor-format.md b/docs/features/software-catalog/descriptor-format.md
index 68cee2352c..ba542d0fd1 100644
--- a/docs/features/software-catalog/descriptor-format.md
+++ b/docs/features/software-catalog/descriptor-format.md
@@ -14,6 +14,8 @@ humans. However, the structure and semantics is the same in both cases.
- [Common to All Kinds: The Envelope](#common-to-all-kinds-the-envelope)
- [Common to All Kinds: The Metadata](#common-to-all-kinds-the-metadata)
- [Kind: Component](#kind-component)
+- [Kind: Template](#kind-template)
+- [Kind: API](#kind-api)
## Overall Shape Of An Entity
@@ -252,6 +254,8 @@ spec:
type: website
lifecycle: production
owner: artist-relations@example.com
+ implementsApis:
+ - artist-api
```
In addition to the [common envelope metadata](#common-to-all-kinds-the-metadata)
@@ -312,6 +316,14 @@ Apart from being a string, the software catalog leaves the format of this field
open to implementers to choose. Most commonly, it is set to the ID or email of a
group of people in an organizational structure.
+### `spec.implementsApis` [optional]
+
+Links APIs that are implemented by the component, e.g. `artist-api`. This field
+is optional.
+
+The software catalog expects a list of one or more strings that references the
+names of other entities of the `kind` `API`.
+
## Kind: Template
Describes the following entity kind:
@@ -422,3 +434,76 @@ specify relative to the `template.yaml` definition.
This is also particularly useful when you have multiple template definitions in
the same repository but only a single `template.yaml` registered in backstage.
+
+## Kind: API
+
+Describes the following entity kind:
+
+| Field | Value |
+| ------------ | ----------------------- |
+| `apiVersion` | `backstage.io/v1alpha1` |
+| `kind` | `API` |
+
+An API describes an interface that can be exposed by a component. The API can be
+defined in different formats, like [OpenAPI](https://swagger.io/specification/),
+[AsyncAPI](https://www.asyncapi.com/docs/specifications/latest/),
+[gRPC](https://developers.google.com/protocol-buffers), or other formats.
+
+Descriptor files for this kind may look as follows.
+
+```yaml
+apiVersion: backstage.io/v1alpha1
+kind: API
+metadata:
+ name: artist-api
+ description: Retrieve artist details
+spec:
+ type: openapi
+ definition: |
+ openapi: "3.0.0"
+ info:
+ version: 1.0.0
+ title: Artist API
+ license:
+ name: MIT
+ servers:
+ - url: http://artist.spotify.net/v1
+ paths:
+ /artists:
+ get:
+ summary: List all artists
+ ...
+```
+
+In addition to the [common envelope metadata](#common-to-all-kinds-the-metadata)
+shape, this kind has the following structure.
+
+### `apiVersion` and `kind` [required]
+
+Exactly equal to `backstage.io/v1alpha1` and `API`, respectively.
+
+### `spec.type` [required]
+
+The type of the API definition as a string, e.g. `openapi`. This field is
+required.
+
+The software catalog accepts any type value, but an organisation should take
+great care to establish a proper taxonomy for these. Tools including Backstage
+itself may read this field and behave differently depending on its value. For
+example, an OpenAPI type API may be displayed using an OpenAPI viewer tooling in
+the Backstage interface.
+
+The current set of well-known and common values for this field is:
+
+- `openapi` - An API definition in YAML or JSON format based on the
+ [OpenAPI](https://swagger.io/specification/) version 2 or version 3 spec.
+- `asyncapi` - An API definition based on the
+ [AsyncAPI](https://www.asyncapi.com/docs/specifications/latest/) spec.
+- `grpc` - An API definition based on
+ [Protocol Buffers](https://developers.google.com/protocol-buffers) to use with
+ [gRPC](https://grpc.io/).
+
+### `spec.definition` [required]
+
+The definition of the API, based on the format defined by `spec.type`. This
+field is required.
diff --git a/packages/app/package.json b/packages/app/package.json
index f42456709c..0e2cb677a4 100644
--- a/packages/app/package.json
+++ b/packages/app/package.json
@@ -5,6 +5,7 @@
"dependencies": {
"@backstage/cli": "^0.1.1-alpha.18",
"@backstage/core": "^0.1.1-alpha.18",
+ "@backstage/plugin-api-docs": "^0.1.1-alpha.18",
"@backstage/plugin-catalog": "^0.1.1-alpha.18",
"@backstage/plugin-circleci": "^0.1.1-alpha.18",
"@backstage/plugin-explore": "^0.1.1-alpha.18",
diff --git a/packages/app/src/components/Root/Root.tsx b/packages/app/src/components/Root/Root.tsx
index a86b92ab43..1d32b6e16e 100644
--- a/packages/app/src/components/Root/Root.tsx
+++ b/packages/app/src/components/Root/Root.tsx
@@ -19,6 +19,7 @@ import PropTypes from 'prop-types';
import { Link, makeStyles } from '@material-ui/core';
import HomeIcon from '@material-ui/icons/Home';
import ExploreIcon from '@material-ui/icons/Explore';
+import ExtensionIcon from '@material-ui/icons/Extension';
import BuildIcon from '@material-ui/icons/BuildRounded';
import RuleIcon from '@material-ui/icons/AssignmentTurnedIn';
import MapIcon from '@material-ui/icons/MyLocation';
@@ -90,6 +91,7 @@ const Root: FC<{}> = ({ children }) => (
{/* Global nav, not org-specific */}
+
{/* End global nav */}
diff --git a/packages/app/src/plugins.ts b/packages/app/src/plugins.ts
index 7e803698fe..4ec19c74f3 100644
--- a/packages/app/src/plugins.ts
+++ b/packages/app/src/plugins.ts
@@ -30,3 +30,4 @@ export { plugin as Rollbar } from '@backstage/plugin-rollbar';
export { plugin as Newrelic } from '@backstage/plugin-newrelic';
export { plugin as TravisCI } from '@roadiehq/backstage-plugin-travis-ci';
export { plugin as Jenkins } from '@backstage/plugin-jenkins';
+export { plugin as ApiDocs } from '@backstage/plugin-api-docs';
diff --git a/packages/catalog-model/examples/hello-world-api.yaml b/packages/catalog-model/examples/hello-world-api.yaml
new file mode 100644
index 0000000000..298b0cb835
--- /dev/null
+++ b/packages/catalog-model/examples/hello-world-api.yaml
@@ -0,0 +1,46 @@
+apiVersion: backstage.io/v1alpha1
+kind: API
+metadata:
+ name: hello-world
+ description: Hello World example for gRPC
+spec:
+ type: grpc
+ definition: |
+ // Copyright 2015 gRPC authors.
+ //
+ // Licensed under the Apache License, Version 2.0 (the "License");
+ // you may not use this file except in compliance with the License.
+ // You may obtain a copy of the License at
+ //
+ // http://www.apache.org/licenses/LICENSE-2.0
+ //
+ // Unless required by applicable law or agreed to in writing, software
+ // distributed under the License is distributed on an "AS IS" BASIS,
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ // See the License for the specific language governing permissions and
+ // limitations under the License.
+
+ syntax = "proto3";
+
+ option java_multiple_files = true;
+ option java_package = "io.grpc.examples.helloworld";
+ option java_outer_classname = "HelloWorldProto";
+ option objc_class_prefix = "HLW";
+
+ package helloworld;
+
+ // The greeting service definition.
+ service Greeter {
+ // Sends a greeting
+ rpc SayHello (HelloRequest) returns (HelloReply) {}
+ }
+
+ // The request message containing the user's name.
+ message HelloRequest {
+ string name = 1;
+ }
+
+ // The response message containing the greetings
+ message HelloReply {
+ string message = 1;
+ }
diff --git a/packages/catalog-model/examples/petstore-api.yaml b/packages/catalog-model/examples/petstore-api.yaml
new file mode 100644
index 0000000000..01953a4a97
--- /dev/null
+++ b/packages/catalog-model/examples/petstore-api.yaml
@@ -0,0 +1,119 @@
+apiVersion: backstage.io/v1alpha1
+kind: API
+metadata:
+ name: petstore
+ description: The petstore API
+spec:
+ type: openapi
+ definition: |
+ openapi: "3.0.0"
+ info:
+ version: 1.0.0
+ title: Swagger Petstore
+ 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/Pet"
+ default:
+ description: unexpected error
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/Error"
+ components:
+ schemas:
+ Pet:
+ type: object
+ required:
+ - id
+ - name
+ properties:
+ id:
+ type: integer
+ format: int64
+ name:
+ type: string
+ tag:
+ type: string
+ Pets:
+ type: array
+ items:
+ $ref: "#/components/schemas/Pet"
+ Error:
+ type: object
+ required:
+ - code
+ - message
+ properties:
+ code:
+ type: integer
+ format: int32
+ message:
+ type: string
diff --git a/packages/catalog-model/examples/petstore-component.yaml b/packages/catalog-model/examples/petstore-component.yaml
new file mode 100644
index 0000000000..5cf0d1f270
--- /dev/null
+++ b/packages/catalog-model/examples/petstore-component.yaml
@@ -0,0 +1,13 @@
+apiVersion: backstage.io/v1alpha1
+kind: Component
+metadata:
+ name: petstore
+ description: Petstore
+spec:
+ type: service
+ lifecycle: experimental
+ owner: pets@example.com
+ implementedApis:
+ - petstore
+ - streetlights
+ - hello-world
diff --git a/packages/catalog-model/examples/streetlights-api.yaml b/packages/catalog-model/examples/streetlights-api.yaml
new file mode 100644
index 0000000000..69a6d101cb
--- /dev/null
+++ b/packages/catalog-model/examples/streetlights-api.yaml
@@ -0,0 +1,217 @@
+apiVersion: backstage.io/v1alpha1
+kind: API
+metadata:
+ name: streetlights
+ description: The Smartylighting Streetlights API allows you to remotely manage the city lights.
+spec:
+ type: asyncapi
+ definition: |
+ asyncapi: 2.0.0
+ info:
+ title: Streetlights API
+ version: '1.0.0'
+ description: |
+ The Smartylighting Streetlights API allows you to remotely manage the city lights.
+
+ ### Check out its awesome features:
+
+ * Turn a specific streetlight on/off š
+ * Dim a specific streetlight š
+ * Receive real-time information about environmental lighting conditions š
+ license:
+ name: Apache 2.0
+ url: https://www.apache.org/licenses/LICENSE-2.0
+
+ servers:
+ production:
+ url: api.streetlights.smartylighting.com:{port}
+ protocol: mqtt
+ description: Test broker
+ variables:
+ port:
+ description: Secure connection (TLS) is available through port 8883.
+ default: '1883'
+ enum:
+ - '1883'
+ - '8883'
+ security:
+ - apiKey: []
+ - supportedOauthFlows:
+ - streetlights:on
+ - streetlights:off
+ - streetlights:dim
+ - openIdConnectWellKnown: []
+
+ defaultContentType: application/json
+
+ channels:
+ smartylighting/streetlights/1/0/event/{streetlightId}/lighting/measured:
+ description: The topic on which measured values may be produced and consumed.
+ parameters:
+ streetlightId:
+ $ref: '#/components/parameters/streetlightId'
+ subscribe:
+ summary: Receive information about environmental lighting conditions of a particular streetlight.
+ operationId: receiveLightMeasurement
+ traits:
+ - $ref: '#/components/operationTraits/kafka'
+ message:
+ $ref: '#/components/messages/lightMeasured'
+
+ smartylighting/streetlights/1/0/action/{streetlightId}/turn/on:
+ parameters:
+ streetlightId:
+ $ref: '#/components/parameters/streetlightId'
+ publish:
+ operationId: turnOn
+ traits:
+ - $ref: '#/components/operationTraits/kafka'
+ message:
+ $ref: '#/components/messages/turnOnOff'
+
+ smartylighting/streetlights/1/0/action/{streetlightId}/turn/off:
+ parameters:
+ streetlightId:
+ $ref: '#/components/parameters/streetlightId'
+ publish:
+ operationId: turnOff
+ traits:
+ - $ref: '#/components/operationTraits/kafka'
+ message:
+ $ref: '#/components/messages/turnOnOff'
+
+ smartylighting/streetlights/1/0/action/{streetlightId}/dim:
+ parameters:
+ streetlightId:
+ $ref: '#/components/parameters/streetlightId'
+ publish:
+ operationId: dimLight
+ traits:
+ - $ref: '#/components/operationTraits/kafka'
+ message:
+ $ref: '#/components/messages/dimLight'
+
+ components:
+ messages:
+ lightMeasured:
+ name: lightMeasured
+ title: Light measured
+ summary: Inform about environmental lighting conditions for a particular streetlight.
+ contentType: application/json
+ traits:
+ - $ref: '#/components/messageTraits/commonHeaders'
+ payload:
+ $ref: "#/components/schemas/lightMeasuredPayload"
+ turnOnOff:
+ name: turnOnOff
+ title: Turn on/off
+ summary: Command a particular streetlight to turn the lights on or off.
+ traits:
+ - $ref: '#/components/messageTraits/commonHeaders'
+ payload:
+ $ref: "#/components/schemas/turnOnOffPayload"
+ dimLight:
+ name: dimLight
+ title: Dim light
+ summary: Command a particular streetlight to dim the lights.
+ traits:
+ - $ref: '#/components/messageTraits/commonHeaders'
+ payload:
+ $ref: "#/components/schemas/dimLightPayload"
+
+ schemas:
+ lightMeasuredPayload:
+ type: object
+ properties:
+ lumens:
+ type: integer
+ minimum: 0
+ description: Light intensity measured in lumens.
+ sentAt:
+ $ref: "#/components/schemas/sentAt"
+ turnOnOffPayload:
+ type: object
+ properties:
+ command:
+ type: string
+ enum:
+ - on
+ - off
+ description: Whether to turn on or off the light.
+ sentAt:
+ $ref: "#/components/schemas/sentAt"
+ dimLightPayload:
+ type: object
+ properties:
+ percentage:
+ type: integer
+ description: Percentage to which the light should be dimmed to.
+ minimum: 0
+ maximum: 100
+ sentAt:
+ $ref: "#/components/schemas/sentAt"
+ sentAt:
+ type: string
+ format: date-time
+ description: Date and time when the message was sent.
+
+ securitySchemes:
+ apiKey:
+ type: apiKey
+ in: user
+ description: Provide your API key as the user and leave the password empty.
+ supportedOauthFlows:
+ type: oauth2
+ description: Flows to support OAuth 2.0
+ flows:
+ implicit:
+ authorizationUrl: 'https://authserver.example/auth'
+ scopes:
+ 'streetlights:on': Ability to switch lights on
+ 'streetlights:off': Ability to switch lights off
+ 'streetlights:dim': Ability to dim the lights
+ password:
+ tokenUrl: 'https://authserver.example/token'
+ scopes:
+ 'streetlights:on': Ability to switch lights on
+ 'streetlights:off': Ability to switch lights off
+ 'streetlights:dim': Ability to dim the lights
+ clientCredentials:
+ tokenUrl: 'https://authserver.example/token'
+ scopes:
+ 'streetlights:on': Ability to switch lights on
+ 'streetlights:off': Ability to switch lights off
+ 'streetlights:dim': Ability to dim the lights
+ authorizationCode:
+ authorizationUrl: 'https://authserver.example/auth'
+ tokenUrl: 'https://authserver.example/token'
+ refreshUrl: 'https://authserver.example/refresh'
+ scopes:
+ 'streetlights:on': Ability to switch lights on
+ 'streetlights:off': Ability to switch lights off
+ 'streetlights:dim': Ability to dim the lights
+ openIdConnectWellKnown:
+ type: openIdConnect
+ openIdConnectUrl: 'https://authserver.example/.well-known'
+
+ parameters:
+ streetlightId:
+ description: The ID of the streetlight.
+ schema:
+ type: string
+
+ messageTraits:
+ commonHeaders:
+ headers:
+ type: object
+ properties:
+ my-app-header:
+ type: integer
+ minimum: 0
+ maximum: 100
+
+ operationTraits:
+ kafka:
+ bindings:
+ kafka:
+ clientId: my-app-id
diff --git a/packages/catalog-model/src/EntityPolicies.ts b/packages/catalog-model/src/EntityPolicies.ts
index 120de5152e..8cc51e5aa8 100644
--- a/packages/catalog-model/src/EntityPolicies.ts
+++ b/packages/catalog-model/src/EntityPolicies.ts
@@ -23,6 +23,7 @@ import {
SchemaValidEntityPolicy,
} from './entity';
import {
+ ApiEntityV1alpha1Policy,
ComponentEntityV1alpha1Policy,
GroupEntityV1alpha1Policy,
LocationEntityV1alpha1Policy,
@@ -78,6 +79,7 @@ export class EntityPolicies implements EntityPolicy {
new GroupEntityV1alpha1Policy(),
new LocationEntityV1alpha1Policy(),
new TemplateEntityV1alpha1Policy(),
+ new ApiEntityV1alpha1Policy(),
]),
]);
}
diff --git a/packages/catalog-model/src/kinds/ApiEntityV1alpha1.test.ts b/packages/catalog-model/src/kinds/ApiEntityV1alpha1.test.ts
new file mode 100644
index 0000000000..48de569213
--- /dev/null
+++ b/packages/catalog-model/src/kinds/ApiEntityV1alpha1.test.ts
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { EntityPolicy } from '../types';
+import {
+ ApiEntityV1alpha1,
+ ApiEntityV1alpha1Policy,
+} from './ApiEntityV1alpha1';
+
+describe('ApiV1alpha1Policy', () => {
+ let entity: ApiEntityV1alpha1;
+ let policy: EntityPolicy;
+
+ beforeEach(() => {
+ entity = {
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'API',
+ metadata: {
+ name: 'test',
+ },
+ spec: {
+ type: 'openapi',
+ definition: `
+openapi: "3.0.0"
+info:
+ version: 1.0.0
+ title: Swagger Petstore
+paths:
+ /pets:
+ get:
+ summary: List all pets
+ operationId: listPets
+ responses:
+ '200':
+ description: A paged array of pets
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/Pets"
+components:
+ schemas:
+ Pet:
+ type: object
+ required:
+ - id
+ - name
+ properties:
+ id:
+ type: integer
+ format: int64
+ name:
+ type: string
+ tag:
+ type: string
+ Pets:
+ type: array
+ items:
+ $ref: "#/components/schemas/Pet"
+`,
+ },
+ };
+ policy = new ApiEntityV1alpha1Policy();
+ });
+
+ it('happy path: accepts valid data', async () => {
+ await expect(policy.enforce(entity)).resolves.toBe(entity);
+ });
+
+ it('silently accepts v1beta1 as well', async () => {
+ (entity as any).apiVersion = 'backstage.io/v1beta1';
+ await expect(policy.enforce(entity)).resolves.toBe(entity);
+ });
+
+ it('rejects unknown apiVersion', async () => {
+ (entity as any).apiVersion = 'backstage.io/v1beta0';
+ await expect(policy.enforce(entity)).rejects.toThrow(/apiVersion/);
+ });
+
+ it('rejects unknown kind', async () => {
+ (entity as any).kind = 'Wizard';
+ await expect(policy.enforce(entity)).rejects.toThrow(/kind/);
+ });
+
+ it('rejects missing type', async () => {
+ delete (entity as any).spec.type;
+ await expect(policy.enforce(entity)).rejects.toThrow(/type/);
+ });
+
+ it('rejects wrong type', async () => {
+ (entity as any).spec.type = 7;
+ await expect(policy.enforce(entity)).rejects.toThrow(/type/);
+ });
+
+ it('rejects empty type', async () => {
+ (entity as any).spec.type = '';
+ await expect(policy.enforce(entity)).rejects.toThrow(/type/);
+ });
+
+ it('rejects missing definition', async () => {
+ delete (entity as any).spec.definition;
+ await expect(policy.enforce(entity)).rejects.toThrow(/definition/);
+ });
+
+ it('rejects wrong definition', async () => {
+ (entity as any).spec.definition = 7;
+ await expect(policy.enforce(entity)).rejects.toThrow(/definition/);
+ });
+
+ it('rejects empty definition', async () => {
+ (entity as any).spec.definition = '';
+ await expect(policy.enforce(entity)).rejects.toThrow(/definition/);
+ });
+});
diff --git a/packages/catalog-model/src/kinds/ApiEntityV1alpha1.ts b/packages/catalog-model/src/kinds/ApiEntityV1alpha1.ts
new file mode 100644
index 0000000000..59e16df9a7
--- /dev/null
+++ b/packages/catalog-model/src/kinds/ApiEntityV1alpha1.ts
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import * as yup from 'yup';
+import type { Entity } from '../entity/Entity';
+import type { EntityPolicy } from '../types';
+
+const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const;
+const KIND = 'API' as const;
+
+export interface ApiEntityV1alpha1 extends Entity {
+ apiVersion: typeof API_VERSION[number];
+ kind: typeof KIND;
+ spec: {
+ type: string;
+ definition: string;
+ };
+}
+
+export class ApiEntityV1alpha1Policy implements EntityPolicy {
+ private schema: yup.Schema;
+
+ constructor() {
+ this.schema = yup.object>({
+ apiVersion: yup.string().required().oneOf(API_VERSION),
+ kind: yup.string().required().equals([KIND]),
+ spec: yup
+ .object({
+ type: yup.string().required().min(1),
+ definition: yup.string().required().min(1),
+ })
+ .required(),
+ });
+ }
+
+ async enforce(envelope: Entity): Promise {
+ return await this.schema.validate(envelope, { strict: true });
+ }
+}
diff --git a/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.test.ts b/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.test.ts
index a60bb2606b..e784934d48 100644
--- a/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.test.ts
+++ b/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.test.ts
@@ -35,6 +35,7 @@ describe('ComponentV1alpha1Policy', () => {
type: 'service',
lifecycle: 'production',
owner: 'me',
+ implementsApis: ['api-0'],
},
};
policy = new ComponentEntityV1alpha1Policy();
diff --git a/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.ts b/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.ts
index 68cf0c61bb..38b9c27720 100644
--- a/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.ts
+++ b/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.ts
@@ -28,6 +28,7 @@ export interface ComponentEntityV1alpha1 extends Entity {
type: string;
lifecycle: string;
owner: string;
+ implementsApis?: string[];
};
}
@@ -43,6 +44,7 @@ export class ComponentEntityV1alpha1Policy implements EntityPolicy {
type: yup.string().required().min(1),
lifecycle: yup.string().required().min(1),
owner: yup.string().required().min(1),
+ implementsApis: yup.array(yup.string()).notRequired(),
})
.required(),
});
diff --git a/packages/catalog-model/src/kinds/index.ts b/packages/catalog-model/src/kinds/index.ts
index 4526e99da4..6e8d27d204 100644
--- a/packages/catalog-model/src/kinds/index.ts
+++ b/packages/catalog-model/src/kinds/index.ts
@@ -34,3 +34,8 @@ export type {
TemplateEntityV1alpha1 as TemplateEntity,
TemplateEntityV1alpha1,
} from './TemplateEntityV1alpha1';
+export { ApiEntityV1alpha1Policy } from './ApiEntityV1alpha1';
+export type {
+ ApiEntityV1alpha1 as ApiEntity,
+ ApiEntityV1alpha1,
+} from './ApiEntityV1alpha1';
diff --git a/packages/cli/config/jest.js b/packages/cli/config/jest.js
index da9b3ec3e6..77d3a84e82 100644
--- a/packages/cli/config/jest.js
+++ b/packages/cli/config/jest.js
@@ -48,8 +48,9 @@ async function getConfig() {
// Default behaviour is to not apply transforms for node_modules, but we still want
// to apply the esm-transformer to .esm.js files, since that's what we use in backstage packages.
+ // The @kyma-project/asyncapi-react library needs to be transformed.
transformIgnorePatterns: [
- '/node_modules/(?!.*\\.(?:esm\\.js|bmp|gif|jpg|jpeg|png|frag|xml|svg)$)',
+ '/node_modules/(?!@kyma-project/asyncapi-react/)(?!.*\\.(?:esm\\.js|bmp|gif|jpg|jpeg|png|frag|xml|svg)$)',
],
};
diff --git a/plugins/api-docs/.eslintrc.js b/plugins/api-docs/.eslintrc.js
new file mode 100644
index 0000000000..13573efa9c
--- /dev/null
+++ b/plugins/api-docs/.eslintrc.js
@@ -0,0 +1,3 @@
+module.exports = {
+ extends: [require.resolve('@backstage/cli/config/eslint')],
+};
diff --git a/plugins/api-docs/README.md b/plugins/api-docs/README.md
new file mode 100644
index 0000000000..7ea87004c3
--- /dev/null
+++ b/plugins/api-docs/README.md
@@ -0,0 +1,9 @@
+# API Documentation
+
+WORK IN PROGRESS
+
+This is an extension for the catalog plugin that provides components to discover and display API entities.
+
+## Links
+
+- (The Backstage homepage)[https://backstage.io]
diff --git a/plugins/api-docs/dev/index.tsx b/plugins/api-docs/dev/index.tsx
new file mode 100644
index 0000000000..812a5585d4
--- /dev/null
+++ b/plugins/api-docs/dev/index.tsx
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { createDevApp } from '@backstage/dev-utils';
+import { plugin } from '../src/plugin';
+
+createDevApp().registerPlugin(plugin).render();
diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json
new file mode 100644
index 0000000000..a599e7e322
--- /dev/null
+++ b/plugins/api-docs/package.json
@@ -0,0 +1,54 @@
+{
+ "name": "@backstage/plugin-api-docs",
+ "version": "0.1.1-alpha.18",
+ "main": "src/index.ts",
+ "types": "src/index.ts",
+ "license": "Apache-2.0",
+ "private": true,
+ "publishConfig": {
+ "access": "public",
+ "main": "dist/index.esm.js",
+ "types": "dist/index.d.ts"
+ },
+ "scripts": {
+ "build": "backstage-cli plugin:build",
+ "start": "backstage-cli plugin:serve",
+ "lint": "backstage-cli lint",
+ "test": "backstage-cli test",
+ "diff": "backstage-cli plugin:diff",
+ "prepack": "backstage-cli prepack",
+ "postpack": "backstage-cli postpack",
+ "clean": "backstage-cli clean"
+ },
+ "dependencies": {
+ "@backstage/catalog-model": "^0.1.1-alpha.18",
+ "@backstage/core": "^0.1.1-alpha.18",
+ "@backstage/plugin-catalog": "^0.1.1-alpha.18",
+ "@backstage/theme": "^0.1.1-alpha.18",
+ "@kyma-project/asyncapi-react": "^0.6.1",
+ "@material-icons/font": "^1.0.2",
+ "@material-ui/core": "^4.9.1",
+ "@material-ui/icons": "^4.9.1",
+ "@material-ui/lab": "4.0.0-alpha.45",
+ "react": "^16.13.1",
+ "react-dom": "^16.13.1",
+ "react-router-dom": "6.0.0-beta.0",
+ "react-use": "^15.3.3",
+ "swagger-ui-react": "^3.31.1"
+ },
+ "devDependencies": {
+ "@backstage/cli": "^0.1.1-alpha.18",
+ "@backstage/dev-utils": "^0.1.1-alpha.18",
+ "@backstage/test-utils": "^0.1.1-alpha.18",
+ "@testing-library/jest-dom": "^5.10.1",
+ "@testing-library/react": "^10.4.1",
+ "@testing-library/user-event": "^12.0.7",
+ "@types/jest": "^26.0.7",
+ "@types/node": "^12.0.0",
+ "@types/swagger-ui-react": "^3.23.3",
+ "jest-fetch-mock": "^3.0.3"
+ },
+ "files": [
+ "dist"
+ ]
+}
diff --git a/plugins/api-docs/src/components/ApiCatalogPage/ApiCatalogLayout.tsx b/plugins/api-docs/src/components/ApiCatalogPage/ApiCatalogLayout.tsx
new file mode 100644
index 0000000000..d846e2b0e2
--- /dev/null
+++ b/plugins/api-docs/src/components/ApiCatalogPage/ApiCatalogLayout.tsx
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Header, Page, pageTheme } from '@backstage/core';
+import React from 'react';
+
+type Props = {
+ children?: React.ReactNode;
+};
+
+const ApiCatalogLayout = ({ children }: Props) => {
+ return (
+
+
+ {children}
+
+ );
+};
+
+export default ApiCatalogLayout;
diff --git a/plugins/api-docs/src/components/ApiCatalogPage/ApiCatalogPage.test.tsx b/plugins/api-docs/src/components/ApiCatalogPage/ApiCatalogPage.test.tsx
new file mode 100644
index 0000000000..19cbaf6038
--- /dev/null
+++ b/plugins/api-docs/src/components/ApiCatalogPage/ApiCatalogPage.test.tsx
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Entity } from '@backstage/catalog-model';
+import { ApiProvider, ApiRegistry, storageApiRef } from '@backstage/core';
+// TODO: Circular ref!
+import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog';
+import { MockStorageApi, wrapInTestApp } from '@backstage/test-utils';
+import { render } from '@testing-library/react';
+import React from 'react';
+import { ApiCatalogPage } from './ApiCatalogPage';
+
+describe('ApiCatalogPage', () => {
+ const catalogApi: Partial = {
+ getEntities: () =>
+ Promise.resolve([
+ {
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'API',
+ metadata: {
+ name: 'Entity1',
+ },
+ },
+ {
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'API',
+ metadata: {
+ name: 'Entity2',
+ },
+ },
+ ] as Entity[]),
+ getLocationByEntity: () =>
+ Promise.resolve({ id: 'id', type: 'github', target: 'url' }),
+ };
+
+ const renderWrapped = (children: React.ReactNode) =>
+ render(
+ wrapInTestApp(
+
+ {children}
+ ,
+ ),
+ );
+
+ // this test right now causes some red lines in the log output when running tests
+ // related to some theme issues in mui-table
+ // https://github.com/mbrn/material-table/issues/1293
+ it('should render', async () => {
+ const { findByText } = renderWrapped();
+ expect(await findByText(/APIs \(2\)/)).toBeInTheDocument();
+ });
+});
diff --git a/plugins/api-docs/src/components/ApiCatalogPage/ApiCatalogPage.tsx b/plugins/api-docs/src/components/ApiCatalogPage/ApiCatalogPage.tsx
new file mode 100644
index 0000000000..f624ec9f92
--- /dev/null
+++ b/plugins/api-docs/src/components/ApiCatalogPage/ApiCatalogPage.tsx
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Content, useApi } from '@backstage/core';
+// TODO: Circular ref
+import { catalogApiRef } from '@backstage/plugin-catalog';
+import React from 'react';
+import { useAsync } from 'react-use';
+import { ApiCatalogTable } from '../ApiCatalogTable/ApiCatalogTable';
+import ApiCatalogLayout from './ApiCatalogLayout';
+
+const CatalogPageContents = () => {
+ const catalogApi = useApi(catalogApiRef);
+ const { loading, error, value: matchingEntities } = useAsync(() => {
+ return catalogApi.getEntities({ kind: 'API' });
+ }, [catalogApi]);
+
+ return (
+
+
+
+
+
+ );
+};
+
+export const ApiCatalogPage = () => ;
diff --git a/plugins/api-docs/src/components/ApiCatalogTable/ApiCatalogTable.test.tsx b/plugins/api-docs/src/components/ApiCatalogTable/ApiCatalogTable.test.tsx
new file mode 100644
index 0000000000..67f6562a56
--- /dev/null
+++ b/plugins/api-docs/src/components/ApiCatalogTable/ApiCatalogTable.test.tsx
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Entity } from '@backstage/catalog-model';
+import { wrapInTestApp } from '@backstage/test-utils';
+import { render } from '@testing-library/react';
+import * as React from 'react';
+import { ApiCatalogTable } from './ApiCatalogTable';
+
+const entites: Entity[] = [
+ {
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'API',
+ metadata: { name: 'api1' },
+ },
+ {
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'API',
+ metadata: { name: 'api2' },
+ },
+ {
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'API',
+ metadata: { name: 'api3' },
+ },
+];
+
+describe('ApiCatalogTable component', () => {
+ it('should render error message when error is passed in props', async () => {
+ const rendered = render(
+ wrapInTestApp(
+ ,
+ ),
+ );
+ const errorMessage = await rendered.findByText(
+ /Error encountered while fetching catalog entities./,
+ );
+ expect(errorMessage).toBeInTheDocument();
+ });
+
+ it('should display entity names when loading has finished and no error occurred', async () => {
+ const rendered = render(
+ wrapInTestApp(
+ ,
+ ),
+ );
+ expect(rendered.getByText(/APIs \(3\)/)).toBeInTheDocument();
+ expect(rendered.getByText(/api1/)).toBeInTheDocument();
+ expect(rendered.getByText(/api2/)).toBeInTheDocument();
+ expect(rendered.getByText(/api3/)).toBeInTheDocument();
+ });
+});
diff --git a/plugins/api-docs/src/components/ApiCatalogTable/ApiCatalogTable.tsx b/plugins/api-docs/src/components/ApiCatalogTable/ApiCatalogTable.tsx
new file mode 100644
index 0000000000..e30583caed
--- /dev/null
+++ b/plugins/api-docs/src/components/ApiCatalogTable/ApiCatalogTable.tsx
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { Entity } from '@backstage/catalog-model';
+import { Table, TableColumn } from '@backstage/core';
+import { Link } from '@material-ui/core';
+import { Alert } from '@material-ui/lab';
+import React from 'react';
+import { generatePath, Link as RouterLink } from 'react-router-dom';
+import { entityRoute } from '../../routes';
+
+const columns: TableColumn[] = [
+ {
+ title: 'Name',
+ field: 'metadata.name',
+ highlight: true,
+ render: (entity: any) => (
+
+ {entity.metadata.name}
+
+ ),
+ },
+ {
+ title: 'Description',
+ field: 'metadata.description',
+ },
+];
+
+type CatalogTableProps = {
+ entities: Entity[];
+ titlePreamble: string;
+ loading: boolean;
+ error?: any;
+};
+
+export const ApiCatalogTable = ({
+ entities,
+ loading,
+ error,
+ titlePreamble,
+}: CatalogTableProps) => {
+ if (error) {
+ return (
+