Merge branch 'master' into kubernetes-common

This commit is contained in:
Juan Lulkin
2021-05-03 13:44:14 +02:00
committed by GitHub
199 changed files with 2735 additions and 851 deletions
+14
View File
@@ -1,5 +1,19 @@
# @backstage/plugin-api-docs
## 0.4.12
### Patch Changes
- 1ce80ff02: Resolve issues with AsyncAPI rendering by updating `@asyncapi/react-component`
to `0.23.0`. The theming of the component is adjusted to the latest styling
changes.
- c614ede9a: Updated README to have up-to-date install instructions.
- 07a7806c3: Added fields filtering in get API entities to avoid the requesting of unused data
- Updated dependencies [9afcac5af]
- Updated dependencies [e0c9ed759]
- Updated dependencies [6eaecbd81]
- @backstage/core@0.7.7
## 0.4.11
### Patch Changes
+6 -5
View File
@@ -15,6 +15,7 @@ Right now, the following API formats are supported:
- [OpenAPI](https://swagger.io/specification/) 2 & 3
- [AsyncAPI](https://www.asyncapi.com/docs/specifications/latest/)
- [GraphQL](https://graphql.org/learn/schema/)
- [JSON Schema](https://json-schema.org/)
Other formats are displayed as plain text, but this can easily be extended.
@@ -28,15 +29,15 @@ To link that a component provides or consumes an API, see the [`providesApis`](h
1. Install the API docs plugin
```bash
# packages/app
# From your Backstage root directory
cd packages/app
yarn add @backstage/plugin-api-docs
```
2. Add the `ApiExplorerPage` extension to the app:
```tsx
// packages/app/src/App.tsx
// In packages/app/src/App.tsx
import { ApiExplorerPage } from '@backstage/plugin-api-docs';
@@ -56,7 +57,7 @@ import {
} from '@backstage/plugin-api-docs';
const apiPage = (
<EntityLayoutWrapper>
<EntityLayout>
<EntityLayout.Route path="/" title="Overview">
<Grid container spacing={3}>
<Grid item md={6}>
@@ -80,7 +81,7 @@ const apiPage = (
</Grid>
</Grid>
</EntityLayout.Route>
</EntityLayoutWrapper>
</EntityLayout>
);
// ...
+15
View File
@@ -27,6 +27,7 @@ import {
} from '../src';
import asyncapiApiEntity from './asyncapi-example-api.yaml';
import graphqlApiEntity from './graphql-example-api.yaml';
import jsonschemaApiEntity from './jsonschema-example-api.yaml';
import openapiApiEntity from './openapi-example-api.yaml';
import otherApiEntity from './other-example-api.yaml';
@@ -41,6 +42,7 @@ createDevApp()
items: [
openapiApiEntity,
asyncapiApiEntity,
jsonschemaApiEntity,
graphqlApiEntity,
otherApiEntity,
],
@@ -87,6 +89,19 @@ createDevApp()
</Page>
),
})
.addPage({
title: 'JSON Schema',
element: (
<Page themeId="home">
<Header title="JSON Schema" />
<Content>
<EntityProvider entity={(jsonschemaApiEntity as any) as Entity}>
<EntityApiDefinitionCard />
</EntityProvider>
</Content>
</Page>
),
})
.addPage({
title: 'GraphQL',
element: (
@@ -0,0 +1,32 @@
apiVersion: backstage.io/v1alpha1
kind: API
metadata:
name: persons
description: Person dataset
spec:
type: jsonschema
lifecycle: experimental
owner: team-c
# From https://json-schema.org/learn/miscellaneous-examples.html
definition: |
{
"$id": "https://example.com/person.schema.json",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Person",
"type": "object",
"properties": {
"firstName": {
"type": "string",
"description": "The person's first name."
},
"lastName": {
"type": "string",
"description": "The person's last name."
},
"age": {
"description": "Age in years which must be equal to or greater than zero.",
"type": "integer",
"minimum": 0
}
}
}
+5 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-api-docs",
"version": "0.4.11",
"version": "0.4.12",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -31,13 +31,16 @@
"dependencies": {
"@asyncapi/react-component": "^0.23.0",
"@backstage/catalog-model": "^0.7.7",
"@backstage/core": "^0.7.6",
"@backstage/core": "^0.7.7",
"@backstage/plugin-catalog-react": "^0.1.4",
"@backstage/theme": "^0.2.6",
"@material-icons/font": "^1.0.2",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
"@stoplight/json-schema-viewer": "^4.0.0-beta.16",
"@stoplight/mosaic": "^1.0.0-beta.46",
"@stoplight/reporter": "^1.10.0",
"@types/react": "^16.9",
"graphiql": "^1.0.0-alpha.10",
"graphql": "^15.3.0",
@@ -16,6 +16,7 @@
import React from 'react';
import { AsyncApiDefinitionWidget } from '../AsyncApiDefinitionWidget';
import { GraphQlDefinitionWidget } from '../GraphQlDefinitionWidget';
import { JsonSchemaDefinitionWidget } from '../JsonSchemaDefinitionWidget';
import { OpenApiDefinitionWidget } from '../OpenApiDefinitionWidget';
export type ApiDefinitionWidget = {
@@ -51,5 +52,13 @@ export function defaultDefinitionWidgets(): ApiDefinitionWidget[] {
<GraphQlDefinitionWidget definition={definition} />
),
},
{
type: 'jsonschema',
title: 'JSON Schema',
rawLanguage: 'json',
component: definition => (
<JsonSchemaDefinitionWidget definition={definition} />
),
},
];
}
@@ -0,0 +1,61 @@
/*
* 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 { renderInTestApp } from '@backstage/test-utils';
import React from 'react';
import { JsonSchemaDefinitionWidget } from './JsonSchemaDefinitionWidget';
describe('<JsonSchemaDefinitionWidget />', () => {
it('renders json schema', async () => {
// From https://json-schema.org/learn/miscellaneous-examples.html
const definition = `
{
"$id": "https://example.com/person.schema.json",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Person",
"type": "object",
"properties": {
"firstName": {
"type": "string",
"description": "The person's first name."
},
"lastName": {
"type": "string",
"description": "The person's last name."
},
"age": {
"description": "Age in years which must be equal to or greater than zero.",
"type": "integer",
"minimum": 0
}
}
}
`;
const { getByText } = await renderInTestApp(
<JsonSchemaDefinitionWidget definition={definition} />,
);
expect(getByText(/lastName/i)).toBeInTheDocument();
expect(getByText(/The person's last name./i)).toBeInTheDocument();
});
it('renders error if definition is missing', async () => {
const { getByText } = await renderInTestApp(
<JsonSchemaDefinitionWidget definition="{}" />,
);
expect(getByText(/No schema defined/i)).toBeInTheDocument();
});
});
@@ -0,0 +1,50 @@
/*
* 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 { useTheme } from '@material-ui/core';
import { JsonSchemaViewer } from '@stoplight/json-schema-viewer';
import { injectStyles, useThemeStore } from '@stoplight/mosaic';
import React, { useMemo } from 'react';
import { useEffectOnce } from 'react-use';
injectStyles();
type Props = {
definition: any;
};
export const JsonSchemaDefinitionWidget = ({ definition }: Props) => {
const schema = useMemo(() => JSON.parse(definition), [definition]);
const theme = useTheme();
const themeStore = useThemeStore();
useEffectOnce(() => {
themeStore.setColor('background', theme.palette.background.paper);
themeStore.setColor('text', theme.palette.text.primary);
themeStore.setColor('primary', theme.palette.primary.main);
themeStore.setColor('success', theme.palette.success.main);
themeStore.setColor('warning', theme.palette.warning.main);
themeStore.setColor('danger', theme.palette.error.main);
themeStore.setMode(theme.palette.type);
});
return (
<JsonSchemaViewer
schema={schema}
emptyText="No schema defined"
defaultExpandedDepth={5}
/>
);
};
@@ -0,0 +1,17 @@
/*
* 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.
*/
export { JsonSchemaDefinitionWidget } from './JsonSchemaDefinitionWidget';
+1
View File
@@ -20,3 +20,4 @@ export * from './AsyncApiDefinitionWidget';
export * from './ComponentsCards';
export * from './OpenApiDefinitionWidget';
export * from './PlainApiDefinitionWidget';
export * from './JsonSchemaDefinitionWidget';
+3 -1
View File
@@ -7,7 +7,9 @@ This backend plugin can be installed to serve static content of a Backstage app.
Add both this package and your local frontend app package as dependencies to your backend, for example
```bash
yarn add @backstage/plugin-app-backend example-app
# From your Backstage root directory
cd packages/backend
yarn add @backstage/plugin-app-backend app
```
By adding the app package as a dependency we ensure that it is built as part of the backend, and that it can be resolved at runtime.
+1 -1
View File
@@ -21,7 +21,7 @@
},
"dependencies": {
"@backstage/catalog-model": "^0.7.6",
"@backstage/core": "^0.7.6",
"@backstage/core": "^0.7.7",
"@backstage/errors": "^0.1.1",
"@backstage/plugin-catalog-react": "^0.1.3",
"@backstage/theme": "^0.2.6",
+10
View File
@@ -1,5 +1,15 @@
# @backstage/plugin-bitrise
## 0.1.2
### Patch Changes
- c614ede9a: Updated README to have up-to-date install instructions.
- Updated dependencies [9afcac5af]
- Updated dependencies [e0c9ed759]
- Updated dependencies [6eaecbd81]
- @backstage/core@0.7.7
## 0.1.1
### Patch Changes
+4 -4
View File
@@ -8,9 +8,9 @@ Welcome to the Bitrise plugin!
## Installation
```sh
# The plugin must be added in the app package
$ cd packages/app
$ yarn add @backstage/plugin-bitrise
# From your Backstage root directory
cd packages/app
yarn add @backstage/plugin-bitrise
```
Bitrise Plugin exposes an entity tab component named `EntityBitriseContent`. You can include it in the
@@ -22,7 +22,7 @@ import { EntityBitriseContent } from '@backstage/plugin-bitrise';
// Farther down at the website declaration
const websiteEntityPage = (
<EntityLayoutWrapper>
<EntityLayout>
{/* Place the following section where you want the tab to appear */}
<EntityLayout.Route path="/bitrise" title="Bitrise">
<EntityBitriseContent />
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-bitrise",
"version": "0.1.1",
"version": "0.1.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -21,7 +21,7 @@
},
"dependencies": {
"@backstage/catalog-model": "^0.7.2",
"@backstage/core": "^0.7.6",
"@backstage/core": "^0.7.7",
"@backstage/plugin-catalog-react": "^0.1.2",
"@backstage/theme": "^0.2.6",
"@material-ui/core": "^4.11.0",
+8
View File
@@ -1,5 +1,13 @@
# @backstage/plugin-catalog-backend
## 0.8.1
### Patch Changes
- a99e0bc42: Entity lifecycle and owner are now indexed by the `DefaultCatalogCollator`. A `locationTemplate` may now optionally be provided to its constructor to reflect a custom catalog entity path in the Backstage frontend.
- Updated dependencies [e1e757569]
- @backstage/plugin-search-backend-node@0.1.4
## 0.8.0
### Minor Changes
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-catalog-backend",
"version": "0.8.0",
"version": "0.8.1",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -35,7 +35,7 @@
"@backstage/config": "^0.1.4",
"@backstage/errors": "^0.1.1",
"@backstage/integration": "^0.5.1",
"@backstage/plugin-search-backend-node": "^0.1.3",
"@backstage/plugin-search-backend-node": "^0.1.4",
"@backstage/search-common": "^0.1.1",
"@octokit/graphql": "^4.5.8",
"@types/express": "^4.17.6",
@@ -156,7 +156,7 @@ describe('github', () => {
describe('getOrganizationRepositories', () => {
it('read repositories', async () => {
const input: QueryResponse = {
organization: {
repositoryOwner: {
repositories: {
nodes: [
{
@@ -20,7 +20,8 @@ import { graphql } from '@octokit/graphql';
// Graphql types
export type QueryResponse = {
organization: Organization;
organization?: Organization;
repositoryOwner?: Organization | User;
};
export type Organization = {
@@ -41,6 +42,7 @@ export type User = {
avatarUrl?: string;
email?: string;
name?: string;
repositories?: Connection<Repository>;
};
export type Team = {
@@ -228,28 +230,27 @@ export async function getOrganizationRepositories(
org: string,
): Promise<{ repositories: Repository[] }> {
const query = `
query repositories($org: String!, $cursor: String) {
organization(login: $org) {
name
repositories(first: 100, after: $cursor) {
nodes {
name
url
isArchived
}
pageInfo {
hasNextPage
endCursor
query repositories($org: String!, $cursor: String) {
repositoryOwner(login: $org) {
login
repositories(first: 100, after: $cursor) {
nodes {
name
url
isArchived
}
pageInfo {
hasNextPage
endCursor
}
}
}
}
}
`;
}`;
const repositories = await queryWithPaging(
client,
query,
r => r.organization?.repositories,
r => r.repositoryOwner?.repositories,
x => x,
{ org },
);
+10
View File
@@ -1,5 +1,15 @@
# @backstage/plugin-catalog-import
## 0.5.4
### Patch Changes
- c614ede9a: Updated README to have up-to-date install instructions.
- Updated dependencies [9afcac5af]
- Updated dependencies [e0c9ed759]
- Updated dependencies [6eaecbd81]
- @backstage/core@0.7.7
## 0.5.3
### Patch Changes
+2 -2
View File
@@ -18,8 +18,8 @@ Some features are not yet available for all supported Git providers.
1. Install the Catalog Import Plugin:
```bash
# packages/app
# From your Backstage root directory
cd packages/app
yarn add @backstage/plugin-catalog-import
```
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-catalog-import",
"version": "0.5.3",
"version": "0.5.4",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -32,7 +32,7 @@
"dependencies": {
"@backstage/catalog-model": "^0.7.6",
"@backstage/catalog-client": "^0.3.9",
"@backstage/core": "^0.7.6",
"@backstage/core": "^0.7.7",
"@backstage/integration": "^0.5.0",
"@backstage/integration-react": "^0.1.1",
"@backstage/plugin-catalog-react": "^0.1.4",
@@ -40,7 +40,7 @@
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
"@octokit/rest": "^18.0.12",
"@octokit/rest": "^18.5.3",
"@types/react": "^16.9",
"git-url-parse": "^11.4.4",
"js-base64": "^3.6.0",
+13
View File
@@ -1,5 +1,18 @@
# @backstage/plugin-catalog
## 0.5.6
### Patch Changes
- 19a4dd710: Removed unused `swr` dependency.
- da546ce00: Support `gridItem` variant for `EntityLinksCard`.
- e0c9ed759: Add `if` prop to `EntityLayout.Route` to conditionally render tabs
- 1a142ae8a: Switch out the time-based personal greeting for a plain title on the catalog index page, and remove the clocks for different timezones.
- Updated dependencies [9afcac5af]
- Updated dependencies [e0c9ed759]
- Updated dependencies [6eaecbd81]
- @backstage/core@0.7.7
## 0.5.5
### Patch Changes
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-catalog",
"version": "0.5.5",
"version": "0.5.6",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -32,7 +32,7 @@
"dependencies": {
"@backstage/catalog-client": "^0.3.10",
"@backstage/catalog-model": "^0.7.7",
"@backstage/core": "^0.7.6",
"@backstage/core": "^0.7.7",
"@backstage/errors": "^0.1.1",
"@backstage/integration": "^0.5.1",
"@backstage/integration-react": "^0.1.1",
+10
View File
@@ -1,5 +1,15 @@
# @backstage/plugin-circleci
## 0.2.13
### Patch Changes
- c614ede9a: Updated README to have up-to-date install instructions.
- Updated dependencies [9afcac5af]
- Updated dependencies [e0c9ed759]
- Updated dependencies [6eaecbd81]
- @backstage/core@0.7.7
## 0.2.12
### Patch Changes
+18 -16
View File
@@ -7,34 +7,35 @@ Website: [https://circleci.com/](https://circleci.com/)
## Setup
1. If you have standalone app (you didn't clone this repo), then do
1. If you have a standalone app (you didn't clone this repo), then do
```bash
# From your Backstage root directory
cd packages/app
yarn add @backstage/plugin-circleci
```
2. Add the `EntityCircleCIContent` extension to the entity page in the app:
2. Add the `EntityCircleCIContent` extension to the entity page in your app:
```tsx
// packages/app/src/components/catalog/EntityPage.tsx
import { EntityCircleCIContent } from '@backstage/plugin-circleci';
// In packages/app/src/components/catalog/EntityPage.tsx
import {
EntityCircleCIContent,
isCircleCIAvailable,
} from '@backstage/plugin-circleci';
// ...
const serviceEntityPage = (
<EntityPageLayout>
...
<EntityLayout.Route path="/circle-ci" title="Circle CI">
// For example in the CI/CD section
const cicdContent = (
<EntitySwitch>
<EntitySwitch.Case if={isCircleCIAvailable}>
<EntityCircleCIContent />
</EntityLayout.Route>
...
</EntityPageLayout>
);
</EntitySwitch.Case>
```
4. Add proxy config:
```yaml
// app-config.yaml
# In app-config.yaml
proxy:
'/circleci/api':
target: https://circleci.com/api/v1.1
@@ -42,10 +43,11 @@ proxy:
Circle-Token: ${CIRCLECI_AUTH_TOKEN}
```
5. Get and provide `CIRCLECI_AUTH_TOKEN` as env variable (https://circleci.com/docs/api/#add-an-api-token)
6. Add `circleci.com/project-slug` annotation to your catalog-info.yaml file in format <git-provider>/<owner>/<project> (https://backstage.io/docs/architecture-decisions/adrs-adr002#format)
5. Get and provide a `CIRCLECI_AUTH_TOKEN` as an environment variable (see the [CircleCI docs](https://circleci.com/docs/api/#add-an-api-token)).
6. Add a `circleci.com/project-slug` annotation to your respective `catalog-info.yaml` files, on the format <git-provider>/<owner>/<project> (https://backstage.io/docs/architecture-decisions/adrs-adr002#format).
```yaml
# Example catalog-info.yaml entity definition file
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-circleci",
"version": "0.2.12",
"version": "0.2.13",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -32,7 +32,7 @@
},
"dependencies": {
"@backstage/catalog-model": "^0.7.3",
"@backstage/core": "^0.7.6",
"@backstage/core": "^0.7.7",
"@backstage/plugin-catalog-react": "^0.1.2",
"@backstage/theme": "^0.2.6",
"@material-ui/core": "^4.11.0",
+1 -1
View File
@@ -32,7 +32,7 @@
"dependencies": {
"@backstage/catalog-model": "^0.7.3",
"@backstage/plugin-catalog-react": "^0.1.2",
"@backstage/core": "^0.7.6",
"@backstage/core": "^0.7.7",
"@backstage/theme": "^0.2.6",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
@@ -1,5 +1,11 @@
# @backstage/plugin-code-coverage-backend
## 0.1.3
### Patch Changes
- d47c2628b: Include migrations
## 0.1.2
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-code-coverage-backend",
"version": "0.1.2",
"version": "0.1.3",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
+1 -1
View File
@@ -22,7 +22,7 @@
"dependencies": {
"@backstage/catalog-model": "^0.7.7",
"@backstage/config": "^0.1.4",
"@backstage/core": "^0.7.6",
"@backstage/core": "^0.7.7",
"@backstage/errors": "^0.1.1",
"@backstage/plugin-catalog-react": "^0.1.4",
"@backstage/theme": "^0.2.6",
+1 -1
View File
@@ -21,7 +21,7 @@
},
"dependencies": {
"@backstage/config": "^0.1.4",
"@backstage/core": "^0.7.6",
"@backstage/core": "^0.7.7",
"@backstage/errors": "^0.1.1",
"@backstage/theme": "^0.2.6",
"@material-ui/core": "^4.11.0",
+11
View File
@@ -1,5 +1,16 @@
# @backstage/plugin-cost-insights
## 0.8.5
### Patch Changes
- b98de52ae: Support a `name` prop for Projects for display purposes
- c614ede9a: Updated README to have up-to-date install instructions.
- Updated dependencies [9afcac5af]
- Updated dependencies [e0c9ed759]
- Updated dependencies [6eaecbd81]
- @backstage/core@0.7.7
## 0.8.4
### Patch Changes
+2
View File
@@ -16,6 +16,8 @@ Learn more with the Backstage blog post [New Cost Insights plugin: The engineer'
## Install
```bash
# From your Backstage root directory
cd packages/app
yarn add @backstage/plugin-cost-insights
```
@@ -33,6 +33,8 @@ Cost Explorer permission policy:
Install the AWS Cost Explorer SDK. The AWS docs recommend using the SDK over making calls to the API directly as it simplifies authentication and provides direct access to commands.
```bash
# From your Backstage root directory
cd packages/app
yarn add @aws-sdk/client-cost-explorer
```
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-cost-insights",
"version": "0.8.4",
"version": "0.8.5",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -31,7 +31,7 @@
},
"dependencies": {
"@backstage/config": "^0.1.3",
"@backstage/core": "^0.7.6",
"@backstage/core": "^0.7.7",
"@backstage/theme": "^0.2.6",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
@@ -46,6 +46,7 @@ describe.each`
engineerCost | ratio | amount | expected
${200_000} | ${0} | ${0} | ${'Negligible'}
${200_000} | ${0} | ${8_333} | ${'Negligible'}
${200_000} | ${undefined} | ${10_000} | ${`~1 ${engineers.unit}`}
${200_000} | ${0.000000001} | ${8_334} | ${`0% or ~1 ${engineers.unit}`}
${200_000} | ${-0.000000001} | ${10_000} | ${`0% or ~1 ${engineers.unit}`}
${200_000} | ${-0.8} | ${10_000} | ${`80% or ~1 ${engineers.unit}`}
@@ -65,6 +66,9 @@ describe.each`
engineerCost | ratio | amount | expected
${200_000} | ${0} | ${0} | ${'Negligible'}
${200_000} | ${0} | ${8_333} | ${'Negligible'}
${200_000} | ${undefined} | ${-1_000} | ${'Negligible'}
${200_000} | ${undefined} | ${1_000} | ${'Negligible'}
${200_000} | ${undefined} | ${10_000} | ${'~$10,000'}
${200_000} | ${0.000000001} | ${8_334} | ${'0% or ~$8,334'}
${200_000} | ${-0.000000001} | ${10_000} | ${'0% or ~$10,000'}
${200_000} | ${-0.8} | ${10_000} | ${'80% or ~$10,000'}
@@ -84,6 +88,8 @@ describe.each`
engineerCost | ratio | amount | expected
${200_000} | ${0} | ${0} | ${'Negligible'}
${200_000} | ${0} | ${8_333} | ${'Negligible'}
${200_000} | ${undefined} | ${1_000} | ${'Negligible'}
${200_000} | ${undefined} | ${10_000} | ${`~2,857 ${carbon.unit}s`}
${200_000} | ${0.000000001} | ${8_334} | ${`0% or ~2,381 ${carbon.unit}s`}
${200_000} | ${-0.000000001} | ${10_000} | ${`0% or ~2,857 ${carbon.unit}s`}
${200_000} | ${-0.8} | ${10_000} | ${`80% or ~2,857 ${carbon.unit}s`}
@@ -29,6 +29,7 @@ import { useCostGrowthStyles as useStyles } from '../../utils/styles';
import { formatPercent, formatCurrency } from '../../utils/formatters';
import { indefiniteArticleOf } from '../../utils/grammar';
import { useConfig, useCurrency } from '../../hooks';
import { notEmpty } from '../../utils/assert';
export type CostGrowthProps = {
change: ChangeStatistic;
@@ -42,31 +43,65 @@ export const CostGrowth = ({ change, duration }: CostGrowthProps) => {
// Only display costs in absolute values
const amount = Math.abs(change.amount);
const ratio = Math.abs(change.ratio);
const ratio = Math.abs(change.ratio ?? NaN);
const rate = rateOf(engineerCost, duration);
const engineers = amount / rate;
const converted = amount / (currency.rate ?? rate);
// If a ratio cannot be calculated, don't format.
const growth = notEmpty(change.ratio)
? growthOf({ ratio: change.ratio, amount: engineers })
: null;
// Determine if growth is significant enough to highlight
const growth = growthOf(change.ratio, engineers);
const classes = classnames({
[styles.excess]: growth === GrowthType.Excess,
[styles.savings]: growth === GrowthType.Savings,
});
const percent = formatPercent(ratio);
let cost = `${percent} or ~${formatCurrency(converted, currency.unit)}`;
// Always display the converted value but use the cost in engineers
// to determine negligibility, as costs should be time-period aware
if (engineers < EngineerThreshold) {
cost = 'Negligible';
} else if (currency.kind === CurrencyType.USD) {
cost = `${percent} or ~${currency.prefix}${formatCurrency(converted)}`;
} else if (amount < 1) {
cost = `less than ${indefiniteArticleOf(['a', 'an'], currency.unit)}`;
return <span className={classes}>Negligible</span>;
}
return <span className={classes}>{cost}</span>;
if (currency.kind === CurrencyType.USD) {
// Do not display percentage if ratio cannot be calculated
if (isNaN(ratio)) {
return (
<span className={classes}>
~{currency.prefix}
{formatCurrency(converted)}
</span>
);
}
return (
<span className={classes}>
{formatPercent(ratio)} or ~{currency.prefix}
{formatCurrency(converted)}
</span>
);
}
if (amount < 1) {
return (
<span className={classes}>
less than {indefiniteArticleOf(['a', 'an'], currency.unit)}
</span>
);
}
// Do not display percentage if ratio cannot be calculated
if (isNaN(ratio)) {
return (
<span className={classes}>
~{formatCurrency(converted, currency.unit)}
</span>
);
}
return (
<span className={classes}>
{formatPercent(ratio)} or ~{formatCurrency(converted, currency.unit)}
</span>
);
};
@@ -21,8 +21,6 @@ import { ChangeThreshold, EngineerThreshold } from '../../types';
describe.each`
ratio | amount | ariaLabel
${-0.1} | ${undefined} | ${'savings'}
${0.01} | ${undefined} | ${'excess'}
${ChangeThreshold.lower} | ${EngineerThreshold} | ${'savings'}
${ChangeThreshold.lower - 0.01} | ${EngineerThreshold} | ${'savings'}
${ChangeThreshold.lower - 0.01} | ${EngineerThreshold + 0.1} | ${'savings'}
@@ -32,7 +30,7 @@ describe.each`
`('growthOf', ({ ratio, amount, ariaLabel }) => {
it(`should display the correct indicator for ${ariaLabel}`, async () => {
const { getByLabelText } = await renderInTestApp(
<CostGrowthIndicator ratio={ratio} amount={amount} />,
<CostGrowthIndicator change={{ ratio, amount }} />,
);
expect(getByLabelText(ariaLabel)).toBeInTheDocument();
});
@@ -40,7 +38,8 @@ describe.each`
describe.each`
ratio | amount
${0} | ${undefined}
${undefined} | ${0}
${0} | ${0}
${ChangeThreshold.lower} | ${0}
${ChangeThreshold.lower + 0.01} | ${EngineerThreshold}
${ChangeThreshold.lower + 0.01} | ${EngineerThreshold + 0.1}
@@ -49,7 +48,7 @@ describe.each`
`('growthOf', ({ ratio, amount }) => {
it('should display the correct indicator for negligible growth', async () => {
const { queryByLabelText } = await renderInTestApp(
<CostGrowthIndicator ratio={ratio} amount={amount} />,
<CostGrowthIndicator change={{ ratio, amount }} />,
);
expect(queryByLabelText('savings')).not.toBeInTheDocument();
expect(queryByLabelText('excess')).not.toBeInTheDocument();
@@ -20,49 +20,33 @@ import { Typography, TypographyProps } from '@material-ui/core';
import { default as ArrowDropUp } from '@material-ui/icons/ArrowDropUp';
import { default as ArrowDropDown } from '@material-ui/icons/ArrowDropDown';
import { growthOf } from '../../utils/change';
import { GrowthType } from '../../types';
import { ChangeStatistic, GrowthType, Maybe } from '../../types';
import { useCostGrowthStyles as useStyles } from '../../utils/styles';
export type CostGrowthIndicatorProps = TypographyProps & {
ratio: number;
amount?: number;
formatter?: (amount: number) => string;
change: ChangeStatistic;
formatter?: (change: ChangeStatistic) => Maybe<string>;
};
export const CostGrowthIndicator = ({
ratio,
amount,
change,
formatter,
className,
...props
}: CostGrowthIndicatorProps) => {
const classes = useStyles();
const growth = growthOf(ratio, amount);
const growth = growthOf(change);
const classNames = classnames(classes.indicator, className, {
[classes.savings]: growth === GrowthType.Savings,
[classes.excess]: growth === GrowthType.Excess,
[classes.savings]: growth === GrowthType.Savings,
});
// Display cost as a factor of engineer cost growth and percentage growth
if (typeof amount === 'number') {
return (
<Typography className={classNames} component="span" {...props}>
{formatter ? formatter(amount) : amount}
{growth === GrowthType.Savings && (
<ArrowDropDown aria-label="savings" />
)}
{growth === GrowthType.Excess && <ArrowDropUp aria-label="excess" />}
</Typography>
);
}
// Display cost as a factor of percent change
return (
<Typography className={classNames} component="span" {...props}>
{formatter ? formatter(ratio) : ratio}
{ratio < 0 && <ArrowDropDown aria-label="savings" />}
{ratio > 0 && <ArrowDropUp aria-label="excess" />}
{formatter ? formatter(change) : change.ratio}
{growth === GrowthType.Excess && <ArrowDropUp aria-label="excess" />}
{growth === GrowthType.Savings && <ArrowDropDown aria-label="savings" />}
</Typography>
);
};
@@ -0,0 +1,153 @@
/*
* Copyright 2021 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 React from 'react';
import { render } from '@testing-library/react';
import { wrapInTestApp } from '@backstage/test-utils';
import { CostOverviewLegend } from './CostOverviewLegend';
import {
MockBillingDateProvider,
MockConfigProvider,
MockFilterProvider,
MockCurrencyProvider,
} from '../../testUtils';
function renderInTestApp(children: JSX.Element) {
return render(
wrapInTestApp(
<MockConfigProvider>
<MockCurrencyProvider>
<MockBillingDateProvider>
<MockFilterProvider>{children}</MockFilterProvider>
</MockBillingDateProvider>
</MockCurrencyProvider>
</MockConfigProvider>,
),
);
}
describe('<CostOverviewLegend />', () => {
it('displays the legend without exploding', async () => {
const { findByText } = renderInTestApp(
<CostOverviewLegend
metric={{
kind: 'msc',
name: 'MSC',
default: false,
}}
metricData={{
id: 'msc',
format: 'number',
aggregation: [],
change: {
ratio: 0,
amount: 0,
},
}}
dailyCostData={{
id: 'mock-id',
aggregation: [],
change: {
amount: 0,
},
}}
/>,
);
expect(await findByText('Cost Trend')).toBeInTheDocument();
expect(await findByText('MSC Trend')).toBeInTheDocument();
});
it('does not display metric legend if metric data is not provided', async () => {
const { findByText, queryByText } = renderInTestApp(
<CostOverviewLegend
metric={{
kind: 'msc',
name: 'MSC',
default: false,
}}
metricData={null}
dailyCostData={{
id: 'mock-id',
aggregation: [],
change: {
amount: 0,
},
}}
/>,
);
expect(await findByText('Cost Trend')).toBeInTheDocument();
expect(queryByText('MSC Trend')).not.toBeInTheDocument();
});
});
describe.each`
ratio | amount | title | expected
${undefined} | ${1_000} | ${'∞'} | ${'Your Excess'}
${undefined} | ${-1_000} | ${'-∞'} | ${'Your Savings'}
`('<CostOverviewLegend />', ({ ratio, amount, title, expected }) => {
it('displays the correct legend if ratio cannot be calculated and costs are within time period', async () => {
const { findByText, findAllByText } = renderInTestApp(
<CostOverviewLegend
metric={{
kind: 'msc',
name: 'MSC',
default: false,
}}
metricData={{
id: 'msc',
format: 'number',
change: {
ratio: ratio,
amount: amount,
},
aggregation: [
{
date: '2020-01-01',
amount: 0,
},
{
date: '2020-07-01', // within default P90D period
amount: amount,
},
],
}}
dailyCostData={{
id: 'mock-id',
change: {
ratio,
amount,
},
aggregation: [
{
date: '2020-01-01',
amount: 0,
},
{
date: '2020-07-01', // within default P90D period
amount: amount,
},
],
}}
/>,
);
expect(await findByText('Cost Trend')).toBeInTheDocument();
expect(await findByText('MSC Trend')).toBeInTheDocument();
expect(await findAllByText(title).then(res => res.length)).toBe(2);
expect(await findByText(expected)).toBeInTheDocument();
});
});
@@ -25,9 +25,9 @@ import {
Metric,
} from '../../types';
import { useLastCompleteBillingDate, useFilters } from '../../hooks';
import { getComparedChange } from '../../utils/change';
import { getComparedChange, choose } from '../../utils/change';
import { mapFiltersToProps } from './selector';
import { formatPercent } from '../../utils/formatters';
import { formatChange } from '../../utils/formatters';
import { CostGrowth } from '../CostGrowth';
type CostOverviewLegendProps = {
@@ -42,9 +42,8 @@ export const CostOverviewLegend = ({
metricData,
}: PropsWithChildren<CostOverviewLegendProps>) => {
const theme = useTheme<CostInsightsTheme>();
const lastCompleteBillingDate = useLastCompleteBillingDate();
const { duration } = useFilters(mapFiltersToProps);
const lastCompleteBillingDate = useLastCompleteBillingDate();
const comparedChange = metricData
? getComparedChange(
@@ -57,23 +56,25 @@ export const CostOverviewLegend = ({
return (
<Box display="flex" flexDirection="row">
<Box mr={2}>
<LegendItem title="Cost Trend" markerColor={theme.palette.blue}>
{formatPercent(dailyCostData.change!.ratio)}
</LegendItem>
</Box>
{metric && metricData && comparedChange && (
{dailyCostData.change && (
<Box mr={2}>
<LegendItem title="Cost Trend" markerColor={theme.palette.blue}>
{formatChange(dailyCostData.change)}
</LegendItem>
</Box>
)}
{metricData && metric && comparedChange && (
<>
<Box mr={2}>
<LegendItem
title={`${metric.name} Trend`}
markerColor={theme.palette.magenta}
>
{formatPercent(metricData.change.ratio)}
{formatChange(metricData.change)}
</LegendItem>
</Box>
<LegendItem
title={comparedChange.ratio <= 0 ? 'Your Savings' : 'Your Excess'}
title={choose(['Your Savings', 'Your Excess'], comparedChange)}
>
<CostGrowth change={comparedChange} duration={duration} />
</LegendItem>
@@ -18,10 +18,10 @@ import React from 'react';
import classnames from 'classnames';
import { Table, TableColumn } from '@backstage/core';
import { Typography } from '@material-ui/core';
import { costFormatter, formatPercent } from '../../utils/formatters';
import { costFormatter, formatChange } from '../../utils/formatters';
import { useEntityDialogStyles as useStyles } from '../../utils/styles';
import { CostGrowthIndicator } from '../CostGrowth';
import { BarChartOptions, Entity } from '../../types';
import { BarChartOptions, ChangeStatistic, Entity } from '../../types';
export type ProductEntityTableOptions = Partial<
Pick<BarChartOptions, 'previousName' | 'currentName'>
@@ -32,7 +32,7 @@ type RowData = {
label: string;
previous: number;
current: number;
ratio: number;
change: ChangeStatistic;
};
function createRenderer(col: keyof RowData, classes: Record<string, string>) {
@@ -41,7 +41,7 @@ function createRenderer(col: keyof RowData, classes: Record<string, string>) {
const rowStyles = classnames(classes.row, {
[classes.rowTotal]: row.id === 'total',
[classes.colFirst]: col === 'label',
[classes.colLast]: col === 'ratio',
[classes.colLast]: col === 'change',
});
switch (col) {
@@ -52,12 +52,12 @@ function createRenderer(col: keyof RowData, classes: Record<string, string>) {
{costFormatter.format(row[col])}
</Typography>
);
case 'ratio':
case 'change':
return (
<CostGrowthIndicator
className={rowStyles}
ratio={row.ratio}
formatter={amount => formatPercent(Math.abs(amount))}
change={row.change}
formatter={formatChange}
/>
);
default:
@@ -75,10 +75,15 @@ function createSorter(field?: keyof Omit<RowData, 'id'>) {
if (a.id === 'total') return 1;
if (b.id === 'total') return 1;
if (field === 'label') return a.label.localeCompare(b.label);
if (field === 'change') {
if (formatChange(a[field]) === '∞' || formatChange(b[field]) === '-∞')
return 1;
if (formatChange(a[field]) === '-∞' || formatChange(b[field]) === '∞')
return -1;
return a[field].ratio! - b[field].ratio!;
}
return field
? a[field] - b[field]
: b.previous + b.current - (a.previous + a.current);
return b.previous + b.current - (a.previous + a.current);
};
}
@@ -134,11 +139,11 @@ export const ProductEntityTable = ({
customSort: createSorter('current'),
},
{
field: 'ratio',
field: 'change',
title: <Typography className={lastColClasses}>Change</Typography>,
align: 'right',
render: createRenderer('ratio', classes),
customSort: createSorter('ratio'),
render: createRenderer('change', classes),
customSort: createSorter('change'),
},
];
@@ -148,14 +153,14 @@ export const ProductEntityTable = ({
label: e.id || 'Unknown',
previous: e.aggregation[0],
current: e.aggregation[1],
ratio: e.change.ratio,
change: e.change,
}))
.concat({
id: 'total',
label: 'Total',
previous: entity.aggregation[0],
current: entity.aggregation[1],
ratio: entity.change.ratio,
change: entity.change,
})
.sort(createSorter());
@@ -40,7 +40,7 @@ import {
findAnyKey,
assertAlways,
} from '../../utils/assert';
import { formatPeriod, formatPercent } from '../../utils/formatters';
import { formatPeriod, formatChange } from '../../utils/formatters';
import {
titleOf,
tooltipItemOf,
@@ -54,6 +54,7 @@ import {
useBarChartLayoutStyles as useLayoutStyles,
} from '../../utils/styles';
import { Duration, Entity, Maybe } from '../../types';
import { choose } from '../../utils/change';
export type ProductInsightsChartProps = {
billingDate: string;
@@ -86,7 +87,6 @@ export const ProductInsightsChart = ({
return breakdowns.length > 0;
}, [entities, activeLabel]);
const legendTitle = `Cost ${entity.change.ratio <= 0 ? 'Savings' : 'Growth'}`;
const costStart = entity.aggregation[0];
const costEnd = entity.aggregation[1];
const resources = entities.map(resourceOf);
@@ -136,7 +136,6 @@ export const ProductInsightsChart = ({
const items = payload.map(tooltipItemOf).filter(notEmpty);
const activeEntity = findAlways(entities, e => e.id === id);
const ratio = activeEntity.change.ratio;
const breakdowns = Object.keys(activeEntity.entities);
if (breakdowns.length) {
@@ -148,11 +147,13 @@ export const ProductInsightsChart = ({
title={title}
subtitle={subtitle}
topRight={
<CostGrowthIndicator
className={classes.indicator}
ratio={ratio}
formatter={formatPercent}
/>
!!activeEntity.change.ratio && (
<CostGrowthIndicator
formatter={formatChange}
change={activeEntity.change}
className={classes.indicator}
/>
)
}
actions={
<Box className={classes.actions}>
@@ -173,11 +174,13 @@ export const ProductInsightsChart = ({
<BarChartTooltip
title={title}
topRight={
<CostGrowthIndicator
className={classes.indicator}
ratio={ratio}
formatter={formatPercent}
/>
!!activeEntity.change.ratio && (
<CostGrowthIndicator
formatter={formatChange}
change={activeEntity.change}
className={classes.indicator}
/>
)
}
content={
id
@@ -197,7 +200,9 @@ export const ProductInsightsChart = ({
return (
<Box className={layoutClasses.wrapper}>
<BarChartLegend costStart={costStart} costEnd={costEnd} options={options}>
<LegendItem title={legendTitle}>
<LegendItem
title={choose(['Cost Savings', 'Cost Excess'], entity.change)}
>
<CostGrowth change={entity.change} duration={duration} />
</LegendItem>
</BarChartLegend>
@@ -294,7 +294,6 @@ export const MockBigQueryInsights: Entity = {
id: 'dataset-c',
aggregation: [0, 10_000],
change: {
ratio: 10_000,
amount: 10_000,
},
entities: {},
@@ -415,7 +414,6 @@ export const MockCloudDataflowInsights: Entity = {
id: 'pipeline-c',
aggregation: [0, 10_000],
change: {
ratio: 10_000,
amount: 10_000,
},
entities: {},
@@ -503,7 +501,6 @@ export const MockCloudStorageInsights: Entity = {
id: 'Mock SKU C',
aggregation: [2_000, 0],
change: {
ratio: -1,
amount: -2000,
},
entities: {},
@@ -515,7 +512,6 @@ export const MockCloudStorageInsights: Entity = {
id: 'bucket-c',
aggregation: [0, 0],
change: {
ratio: 0,
amount: 0,
},
entities: {},
@@ -655,7 +651,6 @@ export const MockComputeEngineInsights: Entity = {
id: 'service-c',
aggregation: [0, 10_000],
change: {
ratio: 10_000,
amount: 10_000,
},
entities: {},
@@ -93,10 +93,16 @@ export function changeOf(aggregation: DateAggregation[]): ChangeStatistic {
const lastAmount = aggregation.length
? aggregation[aggregation.length - 1].amount
: 0;
const ratio =
firstAmount !== 0 ? (lastAmount - firstAmount) / firstAmount : 0;
// if either the first or last amounts are zero, the rate of increase/decrease is infinite
if (!firstAmount || !lastAmount) {
return {
amount: lastAmount - firstAmount,
};
}
return {
ratio: ratio,
ratio: (lastAmount - firstAmount) / firstAmount,
amount: lastAmount - firstAmount,
};
}
@@ -16,7 +16,9 @@
export interface ChangeStatistic {
// The ratio of change from one duration to another, expressed as: (newSum - oldSum) / oldSum
ratio: number;
// If a ratio cannot be calculated - such as when a new or old sum is zero,
// the ratio can be omitted and where applicable, ∞ or -∞ will display based on amount.
ratio?: number;
// The actual USD change between time periods (can be negative if costs decreased)
amount: number;
}
+1 -1
View File
@@ -20,7 +20,7 @@ export function notEmpty<TValue>(
return !isNull(value) && !isUndefined(value);
}
export function isUndefined(value: any): boolean {
export function isUndefined(value: any): value is undefined {
return value === undefined;
}
@@ -32,23 +32,23 @@ const GrowthMap = {
describe.each`
ratio | amount | expected
${0.0} | ${undefined} | ${GrowthType.Negligible}
${undefined} | ${0} | ${GrowthType.Negligible}
${0.0} | ${0} | ${GrowthType.Negligible}
${0.0} | ${EngineerThreshold} | ${GrowthType.Negligible}
${ChangeThreshold.lower} | ${0} | ${GrowthType.Negligible}
${ChangeThreshold.lower + 0.01} | ${undefined} | ${GrowthType.Negligible}
${ChangeThreshold.lower + 0.01} | ${0} | ${GrowthType.Negligible}
${ChangeThreshold.lower + 0.01} | ${EngineerThreshold} | ${GrowthType.Negligible}
${ChangeThreshold.lower + 0.01} | ${EngineerThreshold + 0.1} | ${GrowthType.Negligible}
${ChangeThreshold.lower - 0.01} | ${EngineerThreshold - 0.1} | ${GrowthType.Negligible}
${ChangeThreshold.upper - 0.01} | ${undefined} | ${GrowthType.Negligible}
${ChangeThreshold.lower - 0.01} | ${0} | ${GrowthType.Negligible}
${ChangeThreshold.upper} | ${0} | ${GrowthType.Negligible}
${ChangeThreshold.upper - 0.01} | ${0} | ${GrowthType.Negligible}
${ChangeThreshold.upper + 0.01} | ${EngineerThreshold - 0.1} | ${GrowthType.Negligible}
${ChangeThreshold.lower} | ${undefined} | ${GrowthType.Savings}
${ChangeThreshold.upper + 0.01} | ${0} | ${GrowthType.Negligible}
${ChangeThreshold.lower} | ${EngineerThreshold} | ${GrowthType.Savings}
${ChangeThreshold.lower - 0.01} | ${undefined} | ${GrowthType.Savings}
${ChangeThreshold.lower - 0.01} | ${EngineerThreshold} | ${GrowthType.Savings}
${ChangeThreshold.lower - 0.01} | ${EngineerThreshold + 0.1} | ${GrowthType.Savings}
${ChangeThreshold.upper} | ${undefined} | ${GrowthType.Excess}
${ChangeThreshold.upper} | ${EngineerThreshold} | ${GrowthType.Excess}
${ChangeThreshold.upper + 0.01} | ${undefined} | ${GrowthType.Excess}
${ChangeThreshold.upper + 0.01} | ${EngineerThreshold} | ${GrowthType.Excess}
${ChangeThreshold.upper + 0.01} | ${EngineerThreshold + 0.1} | ${GrowthType.Excess}
`(
@@ -63,7 +63,7 @@ describe.each`
expected: GrowthType;
}) => {
it(`should display ${GrowthMap[expected]}`, () => {
expect(growthOf(ratio, amount)).toBe(expected);
expect(growthOf({ ratio, amount })).toBe(expected);
});
},
);
+31 -10
View File
@@ -27,21 +27,26 @@ import {
import dayjs, { OpUnitType } from 'dayjs';
import durationPlugin from 'dayjs/plugin/duration';
import { inclusiveStartDateOf } from './duration';
import { notEmpty } from './assert';
dayjs.extend(durationPlugin);
// Used for displaying status colors
export function growthOf(ratio: number, amount?: number) {
if (typeof amount === 'number') {
if (amount >= EngineerThreshold && ratio >= ChangeThreshold.upper) {
export function growthOf(change: ChangeStatistic): GrowthType {
const exceedsEngineerThreshold = Math.abs(change.amount) >= EngineerThreshold;
if (notEmpty(change.ratio)) {
if (exceedsEngineerThreshold && change.ratio >= ChangeThreshold.upper) {
return GrowthType.Excess;
}
if (amount >= EngineerThreshold && ratio <= ChangeThreshold.lower) {
if (exceedsEngineerThreshold && change.ratio <= ChangeThreshold.lower) {
return GrowthType.Savings;
}
} else {
if (ratio >= ChangeThreshold.upper) return GrowthType.Excess;
if (ratio <= ChangeThreshold.lower) return GrowthType.Savings;
if (exceedsEngineerThreshold && change.amount > 0) return GrowthType.Excess;
if (exceedsEngineerThreshold && change.amount < 0)
return GrowthType.Savings;
}
return GrowthType.Negligible;
@@ -54,15 +59,24 @@ export function getComparedChange(
duration: Duration,
lastCompleteBillingDate: string, // YYYY-MM-DD,
): ChangeStatistic {
const ratio = dailyCost.change!.ratio - metricData.change.ratio;
const dailyCostRatio = dailyCost.change?.ratio;
const metricDataRatio = metricData.change?.ratio;
const previousPeriodTotal = getPreviousPeriodTotalCost(
dailyCost.aggregation,
duration,
lastCompleteBillingDate,
);
// if either ratio cannot be calculated, no compared ratio can be calculated
if (!notEmpty(dailyCostRatio) || !notEmpty(metricDataRatio)) {
return {
amount: previousPeriodTotal,
};
}
return {
ratio: ratio,
amount: previousPeriodTotal * ratio,
ratio: dailyCostRatio - metricDataRatio,
amount: previousPeriodTotal * (dailyCostRatio - metricDataRatio),
};
}
@@ -78,7 +92,6 @@ export function getPreviousPeriodTotalCost(
? [dayjsDuration.days(), 'day']
: [dayjsDuration.months(), 'month'];
const nextPeriodStart = dayjs(startDate).add(amount, type);
// Add up costs that incurred before the start of the next period.
return aggregation.reduce((acc, costByDate) => {
return dayjs(costByDate.date).isBefore(nextPeriodStart)
@@ -86,3 +99,11 @@ export function getPreviousPeriodTotalCost(
: acc;
}, 0);
}
export function choose<T>(
[savings, excess]: [T, T],
change: ChangeStatistic,
): T {
const isSavings = (change.ratio ?? change.amount) <= 0;
return isSavings ? savings : excess;
}
@@ -16,8 +16,9 @@
import moment from 'moment';
import pluralize from 'pluralize';
import { Duration } from '../types';
import { ChangeStatistic, Duration } from '../types';
import { inclusiveEndDateOf, inclusiveStartDateOf } from '../utils/duration';
import { notEmpty } from './assert';
export type Period = {
periodStart: string;
@@ -78,6 +79,13 @@ export function formatCurrency(amount: number, currency?: string): string {
return currency ? `${numString} ${pluralize(currency, n)}` : numString;
}
export function formatChange(change: ChangeStatistic): string {
if (notEmpty(change.ratio)) {
return formatPercent(Math.abs(change.ratio));
}
return change.amount >= 0 ? '∞' : '-∞';
}
export function formatPercent(n: number): string {
// Number.toFixed shows scientific notation for extreme numbers
if (isNaN(n) || Math.abs(n) < 0.01) {
+10
View File
@@ -1,5 +1,15 @@
# @backstage/plugin-explore
## 0.3.4
### Patch Changes
- c614ede9a: Updated README to have up-to-date install instructions.
- Updated dependencies [9afcac5af]
- Updated dependencies [e0c9ed759]
- Updated dependencies [6eaecbd81]
- @backstage/core@0.7.7
## 0.3.3
### Patch Changes
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-explore",
"version": "0.3.3",
"version": "0.3.4",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -31,7 +31,7 @@
},
"dependencies": {
"@backstage/catalog-model": "^0.7.5",
"@backstage/core": "^0.7.6",
"@backstage/core": "^0.7.7",
"@backstage/plugin-catalog-react": "^0.1.4",
"@backstage/plugin-explore-react": "^0.0.4",
"@backstage/theme": "^0.2.6",
+10
View File
@@ -1,5 +1,15 @@
# @backstage/plugin-fossa
## 0.2.6
### Patch Changes
- c614ede9a: Updated README to have up-to-date install instructions.
- Updated dependencies [9afcac5af]
- Updated dependencies [e0c9ed759]
- Updated dependencies [6eaecbd81]
- @backstage/core@0.7.7
## 0.2.5
### Patch Changes
+2 -2
View File
@@ -9,8 +9,8 @@ The FOSSA Plugin displays code statistics from [FOSSA](https://fossa.com/).
1. Install the FOSSA Plugin:
```bash
# packages/app
# From your Backstage root directory
cd packages/app
yarn add @backstage/plugin-fossa
```
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-fossa",
"version": "0.2.5",
"version": "0.2.6",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -32,7 +32,7 @@
},
"dependencies": {
"@backstage/catalog-model": "^0.7.3",
"@backstage/core": "^0.7.6",
"@backstage/core": "^0.7.7",
"@backstage/errors": "^0.1.1",
"@backstage/plugin-catalog-react": "^0.1.1",
"@backstage/theme": "^0.2.6",
+1 -1
View File
@@ -30,7 +30,7 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/core": "^0.7.6",
"@backstage/core": "^0.7.7",
"@backstage/theme": "^0.2.6",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
+11
View File
@@ -1,5 +1,16 @@
# @backstage/plugin-github-actions
## 0.4.4
### Patch Changes
- 4c42ecca2: Wrap EmptyState in Card
- c614ede9a: Updated README to have up-to-date install instructions.
- Updated dependencies [9afcac5af]
- Updated dependencies [e0c9ed759]
- Updated dependencies [6eaecbd81]
- @backstage/core@0.7.7
## 0.4.3
### Patch Changes
+15 -12
View File
@@ -11,15 +11,15 @@ TBD
### Generic Requirements
1. Provide OAuth credentials:
1. [Create an OAuth App](https://developer.github.com/apps/building-oauth-apps/creating-an-oauth-app/) with callback URL set to `http://localhost:7000/api/auth/github`.
2. Take Client ID and Client Secret from the newly created app's settings page and put them into `AUTH_GITHUB_CLIENT_ID` and `AUTH_GITHUB_CLIENT_SECRET` env variables.
1. [Create an OAuth App](https://developer.github.com/apps/building-oauth-apps/creating-an-oauth-app/) with the callback URL set to `http://localhost:7000/api/auth/github`.
2. Take the Client ID and Client Secret from the newly created app's settings page and put them into `AUTH_GITHUB_CLIENT_ID` and `AUTH_GITHUB_CLIENT_SECRET` environment variables.
2. Annotate your component with a correct GitHub Actions repository and owner:
The annotation key is `github.com/project-slug`.
Example:
```
```yaml
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
@@ -38,6 +38,7 @@ TBD
1. Install the plugin dependency in your Backstage app package:
```bash
# From your Backstage root directory
cd packages/app
yarn add @backstage/plugin-github-actions
```
@@ -45,22 +46,24 @@ yarn add @backstage/plugin-github-actions
2. Add to the app `EntityPage` component:
```tsx
// packages/app/src/components/catalog/EntityPage.tsx
import { EntityGithubActionsContent } from '@backstage/plugin-github-actions';
// In packages/app/src/components/catalog/EntityPage.tsx
import {
EntityGithubActionsContent,
isGithubActionsAvailable,
} from '@backstage/plugin-github-actions';
// ...
// You can add the tab to any number of pages, the service page is shown as an
// example here
const serviceEntityPage = (
<EntityPageLayout>
...
<EntityLayout>
{/* other tabs... */}
<EntityLayout.Route path="/github-actions" title="GitHub Actions">
<EntityGithubActionsContent />
</EntityLayout.Route>
...
</EntityPageLayout>
);
```
2. Run the app with `yarn start` and the backend with `yarn --cwd packages/backend start`, navigate to `/github-actions/`.
3. Run the app with `yarn start` and the backend with `yarn start-backend`.
Then navigate to `/github-actions/` under any entity.
## Features
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-github-actions",
"version": "0.4.3",
"version": "0.4.4",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -34,13 +34,13 @@
"dependencies": {
"@backstage/catalog-model": "^0.7.5",
"@backstage/plugin-catalog-react": "^0.1.4",
"@backstage/core": "^0.7.6",
"@backstage/core": "^0.7.7",
"@backstage/integration": "^0.5.1",
"@backstage/theme": "^0.2.6",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
"@octokit/rest": "^18.0.12",
"@octokit/rest": "^18.5.3",
"moment": "^2.27.0",
"react": "^16.13.1",
"react-dom": "^16.13.1",
@@ -202,13 +202,13 @@ export const WorkflowRunDetails = ({ entity }: { entity: Entity }) => {
<TableCell>
<Typography noWrap>Message</Typography>
</TableCell>
<TableCell>{details.value?.head_commit.message}</TableCell>
<TableCell>{details.value?.head_commit?.message}</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Typography noWrap>Commit ID</Typography>
</TableCell>
<TableCell>{details.value?.head_commit.id}</TableCell>
<TableCell>{details.value?.head_commit?.id}</TableCell>
</TableRow>
<TableRow>
<TableCell>
@@ -231,7 +231,7 @@ export const WorkflowRunDetails = ({ entity }: { entity: Entity }) => {
<TableCell>
<Typography noWrap>Author</Typography>
</TableCell>
<TableCell>{`${details.value?.head_commit.author?.name} (${details.value?.head_commit.author?.email})`}</TableCell>
<TableCell>{`${details.value?.head_commit?.author?.name} (${details.value?.head_commit?.author?.email})`}</TableCell>
</TableRow>
<TableRow>
<TableCell>
+10
View File
@@ -1,5 +1,15 @@
# @backstage/plugin-github-deployments
## 0.1.4
### Patch Changes
- c614ede9a: Updated README to have up-to-date install instructions.
- Updated dependencies [9afcac5af]
- Updated dependencies [e0c9ed759]
- Updated dependencies [6eaecbd81]
- @backstage/core@0.7.7
## 0.1.3
### Patch Changes
+2 -2
View File
@@ -13,8 +13,8 @@ The GitHub Deployments Plugin displays recent deployments from GitHub.
1. Install the GitHub Deployments Plugin.
```bash
# packages/app
# From your Backstage root directory
cd packages/app
yarn add @backstage/plugin-github-deployments
```
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-github-deployments",
"version": "0.1.3",
"version": "0.1.4",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -21,7 +21,7 @@
},
"dependencies": {
"@backstage/catalog-model": "^0.7.6",
"@backstage/core": "^0.7.6",
"@backstage/core": "^0.7.7",
"@backstage/plugin-catalog-react": "^0.1.3",
"@backstage/theme": "^0.2.6",
"@material-ui/core": "^4.11.0",
+1 -1
View File
@@ -31,7 +31,7 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/core": "^0.7.6",
"@backstage/core": "^0.7.7",
"@backstage/theme": "^0.2.6",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
+10
View File
@@ -1,5 +1,15 @@
# @backstage/plugin-graphiql
## 0.2.10
### Patch Changes
- c614ede9a: Updated README to have up-to-date install instructions.
- Updated dependencies [9afcac5af]
- Updated dependencies [e0c9ed759]
- Updated dependencies [6eaecbd81]
- @backstage/core@0.7.7
## 0.2.9
### Patch Changes
+1
View File
@@ -12,6 +12,7 @@ By exposing GraphiQL as a plugin instead of a standalone app, it's possible to p
Start out by installing the plugin in your Backstage app:
```bash
# From your Backstage root directory
cd packages/app
yarn add @backstage/plugin-graphiql
```
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-graphiql",
"description": "Backstage plugin for browsing GraphQL APIs",
"version": "0.2.9",
"version": "0.2.10",
"private": false,
"publishConfig": {
"access": "public",
@@ -31,7 +31,7 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/core": "^0.7.6",
"@backstage/core": "^0.7.7",
"@backstage/theme": "^0.2.6",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
+10
View File
@@ -1,5 +1,15 @@
# @backstage/plugin-jenkins
## 0.4.2
### Patch Changes
- c614ede9a: Updated README to have up-to-date install instructions.
- Updated dependencies [9afcac5af]
- Updated dependencies [e0c9ed759]
- Updated dependencies [6eaecbd81]
- @backstage/core@0.7.7
## 0.4.1
### Patch Changes
+23 -17
View File
@@ -11,25 +11,25 @@ Website: [https://jenkins.io/](https://jenkins.io/)
1. If you have a standalone app (you didn't clone this repo), then do
```bash
# From your Backstage root directory
cd packages/app
yarn add @backstage/plugin-jenkins
```
2. Add the `EntityJenkinsContent` extension to the entity page in the app:
```tsx
// packages/app/src/components/catalog/EntityPage.tsx
import { EntityJenkinsContent } from '@backstage/plugin-circleci';
// In packages/app/src/components/catalog/EntityPage.tsx
import { EntityJenkinsContent } from '@backstage/plugin-jenkins';
// ...
// You can add the tab to any number of pages, the service page is shown as an
// example here
const serviceEntityPage = (
<EntityPageLayout>
...
<EntityLayout>
{/* other tabs... */}
<EntityLayout.Route path="/jenkins" title="Jenkins">
<EntityJenkinsContent />
</EntityLayout.Route>
...
</EntityPageLayout>
);
```
3. Add proxy configuration to `app-config.yaml`
@@ -43,14 +43,18 @@ proxy:
Authorization: Basic ${JENKINS_BASIC_AUTH_HEADER}
```
4. Add an environment variable which contains the Jenkins credentials, (note: use an API token not your password). Here user is the name of the user created in Jenkins.
4. Add an environment variable which contains the Jenkins credentials (NOTE:
use an API token, not your password). Here `user` is the name of the user
created in Jenkins.
```shell
export JENKINS_BASIC_AUTH_HEADER=$(echo -n user:api-token | base64)
```
5. Run app with `yarn start`
6. Add the Jenkins folder annotation to your `catalog-info.yaml`, (note: currently this plugin only supports folders and Git SCM)
5. Run the app with `yarn start`
6. Add the Jenkins folder annotation to your `catalog-info.yaml`, (NOTE:
currently this plugin only supports folders and Git SCM)
```yaml
apiVersion: backstage.io/v1alpha1
@@ -68,11 +72,11 @@ spec:
7. Register your component
8. Click the component in the catalog you should now see Jenkins builds, and a last build result for your master build.
8. Click the component in the catalog. You should now see Jenkins builds, and a
last build result for your master build.
Note:
If you are not using environment variable then you can directly type API token in app-config.yaml
Note: If you are not using environment variables, you can directly type the API
token into `app-config.yaml`.
```yaml
proxy:
@@ -83,7 +87,8 @@ proxy:
Authorization: Basic YWRtaW46MTFlYzI1NmU0Mzg1MDFjM2Y1Yzc2Yjc1MWE3ZTQ3YWY4Mw==
```
YWRtaW46MTFlYzI1NmU0Mzg1MDFjM2Y1Yzc2Yjc1MWE3ZTQ3YWY4Mw== is the base64 of user and it's API token e.g. admin:11ec256e438501c3f5c76b751a7e47af83
The string starting with `YWR...` is the base64 encoding of the user and their
API token, e.g. `admin:11ec256e438501c3f5c76b751a7e47af83`.
## Features
@@ -94,4 +99,5 @@ YWRtaW46MTFlYzI1NmU0Mzg1MDFjM2Y1Yzc2Yjc1MWE3ZTQ3YWY4Mw== is the base64 of user a
## Limitations
- Only works with organization folder projects backed by GitHub
- No pagination support currently, limited to 50 projects - don't run this on a Jenkins with lots of builds
- No pagination support currently, limited to 50 projects - don't run this on a
Jenkins instance with lots of builds
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-jenkins",
"version": "0.4.1",
"version": "0.4.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -32,7 +32,7 @@
},
"dependencies": {
"@backstage/catalog-model": "^0.7.3",
"@backstage/core": "^0.7.6",
"@backstage/core": "^0.7.7",
"@backstage/plugin-catalog-react": "^0.1.3",
"@backstage/theme": "^0.2.6",
"@material-ui/core": "^4.11.0",
+6 -3
View File
@@ -1,6 +1,7 @@
# Kafka Backend
This is the backend part of the Kafka plugin. It responds to Kafka requests from the frontend.
This is the backend part of the Kafka plugin. It responds to Kafka requests
from the frontend.
## Configuration
@@ -16,7 +17,9 @@ A list of the brokers' host names and ports to connect to.
### `ssl` (optional)
Configure TLS connection to the Kafka cluster. The options are passed directly to [tls.connect] and used to create the TLS secure context. Normally these would include `key` and `cert`.
Configure TLS connection to the Kafka cluster. The options are passed directly
to [tls.connect] and used to create the TLS secure context. Normally these
would include `key` and `cert`.
Example:
@@ -39,7 +42,7 @@ kafka:
clusters:
- name: prod
brokers:
- my-cluser:9092
- my-cluster:9092
ssl: true
sasl:
mechanism: plain # or 'scram-sha-256' or 'scram-sha-512'
+15 -23
View File
@@ -7,7 +7,9 @@
1. Run:
```bash
yarn add @backstage/plugin-kafka @backstage/plugin-kafka-backend
# From your Backstage root directory
yarn --cwd packages/app add @backstage/plugin-kafka
yarn --cwd packages/backend add @backstage/plugin-kafka-backend
```
2. Add the plugin backend:
@@ -29,41 +31,31 @@ export default async function createPlugin({
And then add to `packages/backend/src/index.ts`:
```js
// ...
// In packages/backend/src/index.ts
import kafka from './plugins/kafka';
// ...
async function main() {
// ...
const kafkaEnv = useHotMemoize(module, () => createEnv('kafka'));
// ...
const apiRouter = Router();
// ...
apiRouter.use('/kafka', await kafka(kafkaEnv));
// ...
```
3. Add the plugin frontend to `packages/app/src/plugin.ts`:
```js
export { plugin as Kafka } from '@backstage/plugin-kafka';
```
4. Register the plugin frontend router in `packages/app/src/components/catalog/EntityPage.tsx`:
3. Add the plugin as a tab to your service entities:
```jsx
import { Router as KafkaRouter } from '@backstage/plugin-kafka';
// In packages/app/src/components/catalog/EntityPage.tsx
import { EntityKafkaContent } from '@backstage/plugin-kafka';
// Then, somewhere inside <EntityPageLayout>
<EntityPageLayout.Content
path="/kafka/*"
title="Kafka"
element={<KafkaRouter entity={entity} />}
/>;
const serviceEntityPage = (
<EntityLayout>
{/* other tabs... */}
<EntityLayout.Route path="/kafka" title="Kafka">
<EntityKafkaContent />
</EntityLayout.Route>
```
5. Add broker configs for the backend in your `app-config.yaml` (see
4. Add broker configs for the backend in your `app-config.yaml` (see
[kafka-backend](https://github.com/backstage/backstage/blob/master/plugins/kafka-backend/README.md)
for more options):
@@ -76,7 +68,7 @@ kafka:
- localhost:9092
```
6. Add `kafka.apache.org/consumer-groups` annotation to your services:
5. Add the `kafka.apache.org/consumer-groups` annotation to your services:
```yaml
apiVersion: backstage.io/v1alpha1
+1 -1
View File
@@ -21,7 +21,7 @@
},
"dependencies": {
"@backstage/catalog-model": "^0.7.4",
"@backstage/core": "^0.7.6",
"@backstage/core": "^0.7.7",
"@backstage/plugin-catalog-react": "^0.1.1",
"@backstage/theme": "^0.2.6",
"@material-ui/core": "^4.11.0",
+2 -2
View File
@@ -32,9 +32,9 @@
"dependencies": {
"@backstage/catalog-model": "^0.7.4",
"@backstage/config": "^0.1.4",
"@backstage/core": "^0.7.6",
"@backstage/core": "^0.7.7",
"@backstage/plugin-catalog-react": "^0.1.3",
"@backstage/plugin-kubernetes-common": "^0.1.0",
"@backstage/plugin-catalog-react": "^0.1.3",
"@backstage/theme": "^0.2.6",
"@kubernetes/client-node": "^0.14.0",
"@material-ui/core": "^4.11.0",
+10
View File
@@ -1,5 +1,15 @@
# @backstage/plugin-lighthouse
## 0.2.15
### Patch Changes
- c614ede9a: Updated README to have up-to-date install instructions.
- Updated dependencies [9afcac5af]
- Updated dependencies [e0c9ed759]
- Updated dependencies [6eaecbd81]
- @backstage/core@0.7.7
## 0.2.14
### Patch Changes
+32 -41
View File
@@ -28,20 +28,21 @@ _It's likely you will need to [enable CORS](https://developer.mozilla.org/en-US/
When you have an instance running that Backstage can hook into, first install the plugin into your app:
```sh
$ yarn add @backstage/plugin-lighthouse
# From your Backstage root directory
cd packages/app
yarn add @backstage/plugin-lighthouse
```
Modify your app routes in `App.tsx` to include the `LighthousePage` component exported from the plugin, for example:
```tsx
// At the top imports
// In packages/app/src/App.tsx
import { LighthousePage } from '@backstage/plugin-lighthouse';
<FlatRoutes>
// ...
<Route path="/lighthouse" element={<LighthousePage />} />
// ...
</FlatRoutes>;
const routes = (
<FlatRoutes>
{/* ...other routes */}
<Route path="/lighthouse" element={<LighthousePage />} />
```
Then configure the `lighthouse-audit-service` URL in your [`app-config.yaml`](https://github.com/backstage/backstage/blob/master/app-config.yaml).
@@ -63,55 +64,45 @@ kind: Component
metadata:
# ...
annotations:
# ...
lighthouse.com/website-url: # A single website url e.g. https://backstage.io/
```
> NOTE: The plugin only supports one website URL per component at this time.
Add a **Lighthouse tab** to the EntityPage:
Add a Lighthouse tab to the entity page:
```tsx
// packages/app/src/components/catalog/EntityPage.tsx
import { EmbeddedRouter as LighthouseRouter } from '@backstage/plugin-lighthouse';
// In packages/app/src/components/catalog/EntityPage.tsx
import { EntityLighthouseContent } from '@backstage/plugin-lighthouse';
// ...
const WebsiteEntityPage = ({ entity }: { entity: Entity }) => (
<EntityPageLayout>
// ...
<EntityPageLayout.Content
path="/lighthouse/*"
title="Lighthouse"
element={<LighthouseRouter entity={entity} />}
/>
</EntityPageLayout>
);
const websiteEntityPage = (
<EntityLayout>
{/* other tabs... */}
<EntityLayout.Route path="/lighthouse" title="Lighthouse">
<EntityLighthouseContent />
</EntityLayout.Route>
```
> NOTE: The embedded router renders page content without a header section allowing it to be rendered within a
> catalog plugin page.
> NOTE: The embedded router renders page content without a header section
> allowing it to be rendered within a catalog plugin page.
Add a **Lighthouse card** to the overview tab on the EntityPage:
```tsx
// packages/app/src/components/catalog/EntityPage.tsx
// In packages/app/src/components/catalog/EntityPage.tsx
import {
LastLighthouseAuditCard,
isPluginApplicableToEntity as isLighthouseAvailable,
EntityLastLighthouseAuditCard,
isLighthouseAvailable,
} from '@backstage/plugin-lighthouse';
// ...
const OverviewContent = ({ entity }: { entity: Entity }) => (
<Grid container spacing={3}>
// ...
{isLighthouseAvailable(entity) && (
<Grid item sm={4}>
<LastLighthouseAuditCard />
</Grid>
)}
</Grid>
);
const overviewContent = (
<Grid container spacing={3} alignItems="stretch">
{/* ...other content */}
<EntitySwitch>
<EntitySwitch.Case if={isLighthouseAvailable}>
<Grid item md={6}>
<EntityLastLighthouseAuditCard />
</Grid>
</EntitySwitch.Case>
</EntitySwitch>
```
Link Lighthouse
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-lighthouse",
"version": "0.2.14",
"version": "0.2.15",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -33,7 +33,7 @@
"dependencies": {
"@backstage/catalog-model": "^0.7.3",
"@backstage/config": "^0.1.4",
"@backstage/core": "^0.7.6",
"@backstage/core": "^0.7.7",
"@backstage/plugin-catalog-react": "^0.1.2",
"@backstage/theme": "^0.2.6",
"@material-ui/core": "^4.11.0",
@@ -46,7 +46,8 @@ with the environment variable \`LAS_CORS\` set to \`true\`._
When you have an instance running that Backstage can hook into, first install the plugin into your app:
\`\`\`sh
$ yarn add @backstage/plugin-lighthouse
cd packages/app
yarn add @backstage/plugin-lighthouse
\`\`\`
Modify your app routes in \`App.tsx\` to include the \`LighthousePage\` component exported from the plugin, for example:
+1 -1
View File
@@ -31,7 +31,7 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/core": "^0.7.6",
"@backstage/core": "^0.7.7",
"@backstage/theme": "^0.2.6",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
+1 -1
View File
@@ -21,7 +21,7 @@
},
"dependencies": {
"@backstage/catalog-model": "^0.7.6",
"@backstage/core": "^0.7.6",
"@backstage/core": "^0.7.7",
"@backstage/core-api": "^0.2.16",
"@backstage/plugin-catalog-react": "^0.1.4",
"@backstage/theme": "^0.2.6",
+10
View File
@@ -1,5 +1,15 @@
# @backstage/plugin-pagerduty
## 0.3.3
### Patch Changes
- c614ede9a: Updated README to have up-to-date install instructions.
- Updated dependencies [9afcac5af]
- Updated dependencies [e0c9ed759]
- Updated dependencies [6eaecbd81]
- @backstage/core@0.7.7
## 0.3.2
### Patch Changes
+2
View File
@@ -19,6 +19,8 @@ This plugin provides:
Install the plugin:
```bash
# From your Backstage root directory
cd packages/app
yarn add @backstage/plugin-pagerduty
```
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-pagerduty",
"version": "0.3.2",
"version": "0.3.3",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -31,7 +31,7 @@
},
"dependencies": {
"@backstage/catalog-model": "^0.7.3",
"@backstage/core": "^0.7.6",
"@backstage/core": "^0.7.7",
"@backstage/plugin-catalog-react": "^0.1.1",
"@backstage/theme": "^0.2.6",
"@material-ui/core": "^4.11.0",
@@ -59,8 +59,8 @@ describe('buildMiddleware', () => {
mockCreateProxyMiddleware.mockClear();
});
it('accepts strings', async () => {
buildMiddleware('/api/', logger, 'test', 'http://mocked');
it('accepts strings prefixed by /', async () => {
buildMiddleware('/proxy', logger, '/test', 'http://mocked');
expect(createProxyMiddleware).toHaveBeenCalledTimes(1);
@@ -74,13 +74,53 @@ describe('buildMiddleware', () => {
expect(filter('', { method: 'PATCH', headers: {} })).toBe(true);
expect(filter('', { method: 'DELETE', headers: {} })).toBe(true);
expect(fullConfig.pathRewrite).toEqual({ '^/api/test/': '/' });
expect(fullConfig.pathRewrite).toEqual({ '^/proxy/test/': '/' });
expect(fullConfig.changeOrigin).toBe(true);
expect(fullConfig.logProvider!(logger)).toBe(logger);
});
it('accepts routes not prefixed with / when path is not suffixed with /', async () => {
buildMiddleware('/proxy', logger, 'test', 'http://mocked');
expect(createProxyMiddleware).toHaveBeenCalledTimes(1);
const [filter, fullConfig] = mockCreateProxyMiddleware.mock.calls[0] as [
(pathname: string, req: Partial<http.IncomingMessage>) => boolean,
ProxyMiddlewareConfig,
];
expect(filter('', { method: 'GET', headers: {} })).toBe(true);
expect(filter('', { method: 'POST', headers: {} })).toBe(true);
expect(filter('', { method: 'PUT', headers: {} })).toBe(true);
expect(filter('', { method: 'PATCH', headers: {} })).toBe(true);
expect(filter('', { method: 'DELETE', headers: {} })).toBe(true);
expect(fullConfig.pathRewrite).toEqual({ '^/proxy/test/': '/' });
expect(fullConfig.changeOrigin).toBe(true);
expect(fullConfig.logProvider!(logger)).toBe(logger);
});
it('accepts routes prefixed with / when path is suffixed with /', async () => {
buildMiddleware('/proxy/', logger, '/test', 'http://mocked');
expect(createProxyMiddleware).toHaveBeenCalledTimes(1);
const [filter, fullConfig] = mockCreateProxyMiddleware.mock.calls[0] as [
(pathname: string, req: Partial<http.IncomingMessage>) => boolean,
ProxyMiddlewareConfig,
];
expect(filter('', { method: 'GET', headers: {} })).toBe(true);
expect(filter('', { method: 'POST', headers: {} })).toBe(true);
expect(filter('', { method: 'PUT', headers: {} })).toBe(true);
expect(filter('', { method: 'PATCH', headers: {} })).toBe(true);
expect(filter('', { method: 'DELETE', headers: {} })).toBe(true);
expect(fullConfig.pathRewrite).toEqual({ '^/proxy/test/': '/' });
expect(fullConfig.changeOrigin).toBe(true);
expect(fullConfig.logProvider!(logger)).toBe(logger);
});
it('limits allowedMethods', async () => {
buildMiddleware('/api/', logger, 'test', {
buildMiddleware('/proxy', logger, '/test', {
target: 'http://mocked',
allowedMethods: ['GET', 'DELETE'],
});
@@ -97,13 +137,13 @@ describe('buildMiddleware', () => {
expect(filter('', { method: 'PATCH', headers: {} })).toBe(false);
expect(filter('', { method: 'DELETE', headers: {} })).toBe(true);
expect(fullConfig.pathRewrite).toEqual({ '^/api/test/': '/' });
expect(fullConfig.pathRewrite).toEqual({ '^/proxy/test/': '/' });
expect(fullConfig.changeOrigin).toBe(true);
expect(fullConfig.logProvider!(logger)).toBe(logger);
});
it('permits default headers', async () => {
buildMiddleware('/api/', logger, 'test', {
buildMiddleware('/proxy', logger, '/test', {
target: 'http://mocked',
});
@@ -143,7 +183,7 @@ describe('buildMiddleware', () => {
});
it('permits default and configured headers', async () => {
buildMiddleware('/api/', logger, 'test', {
buildMiddleware('/proxy', logger, '/test', {
target: 'http://mocked',
headers: {
Authorization: 'my-token',
@@ -176,7 +216,7 @@ describe('buildMiddleware', () => {
});
it('permits configured headers', async () => {
buildMiddleware('/api/', logger, 'test', {
buildMiddleware('/proxy', logger, '/test', {
target: 'http://mocked',
allowedHeaders: ['authorization', 'cookie'],
});
@@ -208,7 +248,7 @@ describe('buildMiddleware', () => {
});
it('responds default headers', async () => {
buildMiddleware('/api/', logger, 'test', {
buildMiddleware('/proxy', logger, '/test', {
target: 'http://mocked',
});
@@ -251,7 +291,7 @@ describe('buildMiddleware', () => {
});
it('responds configured headers', async () => {
buildMiddleware('/api/', logger, 'test', {
buildMiddleware('/proxy', logger, '/test', {
target: 'http://mocked',
allowedHeaders: ['set-cookie'],
});
@@ -282,10 +322,10 @@ describe('buildMiddleware', () => {
it('rejects malformed target URLs', async () => {
expect(() =>
buildMiddleware('/api/', logger, 'test', 'backstage.io'),
buildMiddleware('/proxy', logger, '/test', 'backstage.io'),
).toThrowError(/Proxy target is not a valid URL/);
expect(() =>
buildMiddleware('/api/', logger, 'test', { target: 'backstage.io' }),
buildMiddleware('/proxy', logger, '/test', { target: 'backstage.io' }),
).toThrowError(/Proxy target is not a valid URL/);
});
});
+15 -1
View File
@@ -77,10 +77,24 @@ export function buildMiddleware(
`Proxy target is not a valid URL: ${fullConfig.target ?? ''}`,
);
}
// Default is to do a path rewrite that strips out the proxy's path prefix
// and the rest of the route.
if (fullConfig.pathRewrite === undefined) {
const routeWithSlash = route.endsWith('/') ? route : `${route}/`;
let routeWithSlash = route.endsWith('/') ? route : `${route}/`;
if (!pathPrefix.endsWith('/') && !routeWithSlash.startsWith('/')) {
// Need to insert a / between pathPrefix and routeWithSlash
routeWithSlash = `/${routeWithSlash}`;
} else if (pathPrefix.endsWith('/') && routeWithSlash.startsWith('/')) {
// Never expect this to happen at this point in time as
// pathPrefix is set using `getExternalBaseUrl` which "Returns the
// external HTTP base backend URL for a given plugin,
// **without a trailing slash.**". But in case this changes in future, we
// need to drop a / on either pathPrefix or routeWithSlash
routeWithSlash = routeWithSlash.substring(1);
}
fullConfig.pathRewrite = {
[`^${pathPrefix}${routeWithSlash}`]: '/',
};
+10
View File
@@ -1,5 +1,15 @@
# @backstage/plugin-register-component
## 0.2.14
### Patch Changes
- c614ede9a: Updated README to have up-to-date install instructions.
- Updated dependencies [9afcac5af]
- Updated dependencies [e0c9ed759]
- Updated dependencies [6eaecbd81]
- @backstage/core@0.7.7
## 0.2.13
### Patch Changes
+1
View File
@@ -19,6 +19,7 @@ When installed it is accessible on [localhost:3000/register-component](localhost
1. Install plugin and its dependency `plugin-catalog`
```bash
# From your Backstage root directory
cd packages/app
yarn add @backstage/plugin-register-component
```
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-register-component",
"version": "0.2.13",
"version": "0.2.14",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -31,7 +31,7 @@
},
"dependencies": {
"@backstage/catalog-model": "^0.7.5",
"@backstage/core": "^0.7.6",
"@backstage/core": "^0.7.7",
"@backstage/plugin-catalog-react": "^0.1.4",
"@backstage/theme": "^0.2.6",
"@material-ui/core": "^4.11.0",
@@ -39,7 +39,7 @@
"@material-ui/lab": "4.0.0-alpha.45",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-hook-form": "^6.6.0",
"react-hook-form": "^6.15.4",
"react-router": "6.0.0-beta.0",
"react-router-dom": "6.0.0-beta.0",
"react-use": "^15.3.3"
+10
View File
@@ -1,5 +1,15 @@
# @backstage/plugin-rollbar
## 0.3.4
### Patch Changes
- c614ede9a: Updated README to have up-to-date install instructions.
- Updated dependencies [9afcac5af]
- Updated dependencies [e0c9ed759]
- Updated dependencies [6eaecbd81]
- @backstage/core@0.7.7
## 0.3.3
### Patch Changes
+5 -7
View File
@@ -9,25 +9,23 @@ Website: [https://rollbar.com/](https://rollbar.com/)
2. If you have standalone app (you didn't clone this repo), then do
```bash
# From your Backstage root directory
cd packages/app
yarn add @backstage/plugin-rollbar
```
3. Add to the app `EntityPage` component:
```tsx
// packages/app/src/components/catalog/EntityPage.tsx
// In packages/app/src/components/catalog/EntityPage.tsx
import { EntityRollbarContent } from '@backstage/plugin-rollbar';
// ...
const serviceEntityPage = (
<EntityPageLayout>
...
<EntityLayout>
{/* other tabs... */}
<EntityLayout.Route path="/rollbar" title="Rollbar">
<EntityRollbarContent />
</EntityLayout.Route>
...
</EntityPageLayout>
);
```
4. Setup the `app-config.yaml` and account token environment variable
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-rollbar",
"version": "0.3.3",
"version": "0.3.4",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -32,7 +32,7 @@
},
"dependencies": {
"@backstage/catalog-model": "^0.7.3",
"@backstage/core": "^0.7.6",
"@backstage/core": "^0.7.7",
"@backstage/plugin-catalog-react": "^0.1.1",
"@backstage/theme": "^0.2.6",
"@material-ui/core": "^4.11.0",
+6
View File
@@ -1,5 +1,11 @@
# @backstage/plugin-scaffolder-backend
## 0.10.1
### Patch Changes
- a1783f306: Added the `nebula-preview` preview to `Octokit` for repository visibility.
## 0.10.0
### Minor Changes
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-scaffolder-backend",
"version": "0.10.0",
"version": "0.10.1",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -37,7 +37,7 @@
"@backstage/integration": "^0.5.1",
"@gitbeaker/core": "^28.0.2",
"@gitbeaker/node": "^28.0.2",
"@octokit/rest": "^18.0.12",
"@octokit/rest": "^18.5.3",
"@types/dockerode": "^3.2.1",
"@types/express": "^4.17.6",
"@types/git-url-parse": "^9.0.0",
+12
View File
@@ -1,5 +1,17 @@
# @backstage/plugin-scaffolder
## 0.9.2
### Patch Changes
- f6efa71ee: Enable starred templates on Scaffolder frontend
- 19a4dd710: Removed unused `swr` dependency.
- 23769512a: Support `anyOf`, `oneOf` and `allOf` schemas in the scaffolder template.
- Updated dependencies [9afcac5af]
- Updated dependencies [e0c9ed759]
- Updated dependencies [6eaecbd81]
- @backstage/core@0.7.7
## 0.9.1
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-scaffolder",
"version": "0.9.1",
"version": "0.9.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -33,7 +33,7 @@
"@backstage/catalog-client": "^0.3.10",
"@backstage/catalog-model": "^0.7.7",
"@backstage/config": "^0.1.4",
"@backstage/core": "^0.7.6",
"@backstage/core": "^0.7.7",
"@backstage/integration": "^0.5.1",
"@backstage/integration-react": "^0.1.1",
"@backstage/plugin-catalog-react": "^0.1.4",
@@ -44,7 +44,7 @@
"@rjsf/core": "^2.4.0",
"@rjsf/material-ui": "^2.4.0",
"classnames": "^2.2.6",
"json-schema": "^0.2.5",
"json-schema": "^0.3.0",
"git-url-parse": "^11.4.4",
"humanize-duration": "^3.25.1",
"immer": "^9.0.1",
+6
View File
@@ -1,5 +1,11 @@
# @backstage/plugin-search-backend-node
## 0.1.4
### Patch Changes
- e1e757569: Introduced Scheduler which is responsible for adding new tasks to a schedule together with it's interval timer as well as starting and stopping the scheduler processes.
## 0.1.3
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-search-backend-node",
"version": "0.1.3",
"version": "0.1.4",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
+10
View File
@@ -1,5 +1,15 @@
# @backstage/plugin-search
## 0.3.5
### Patch Changes
- dcd54c7cd: Use `RouteRef` to generate path to search page.
- Updated dependencies [9afcac5af]
- Updated dependencies [e0c9ed759]
- Updated dependencies [6eaecbd81]
- @backstage/core@0.7.7
## 0.3.4
### Patch Changes
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-search",
"version": "0.3.4",
"version": "0.3.5",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -29,7 +29,7 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/core": "^0.7.6",
"@backstage/core": "^0.7.7",
"@backstage/catalog-model": "^0.7.3",
"@backstage/plugin-catalog-react": "^0.1.2",
"@backstage/search-common": "^0.1.1",

Some files were not shown because too many files have changed in this diff Show More