merge
This commit is contained in:
+3
-3
@@ -2,16 +2,16 @@
|
||||
|
||||
Backstage is a single-page application composed of a set of plugins.
|
||||
|
||||
Our goal for the plugin ecosystem is that the definition of a plugin is flexible enough to allow you to expose pretty much any kind of infrastructure or software development tool as a plugin in Backstage. By following strong [design guidelines](https://github.com/spotify/backstage/blob/master/docs/dls/design.md) we ensure the overall user experience stays consistent between plugins.
|
||||
Our goal for the plugin ecosystem is that the definition of a plugin is flexible enough to allow you to expose pretty much any kind of infrastructure or software development tool as a plugin in Backstage. By following strong [design guidelines](https://github.com/backstage/backstage/blob/master/docs/dls/design.md) we ensure the overall user experience stays consistent between plugins.
|
||||
|
||||

|
||||
|
||||
## Creating a plugin
|
||||
|
||||
To create a plugin, follow the steps outlined [here](https://github.com/spotify/backstage/blob/master/docs/plugins/create-a-plugin.md).
|
||||
To create a plugin, follow the steps outlined [here](https://github.com/backstage/backstage/blob/master/docs/plugins/create-a-plugin.md).
|
||||
|
||||
## Suggesting a plugin
|
||||
|
||||
If you start developing a plugin that you aim to release as open source, we suggest that you create a [new Issue](https://github.com/spotify/backstage/issues/new?template=plugin_template.md). This helps the community know what plugins are in development.
|
||||
If you start developing a plugin that you aim to release as open source, we suggest that you create a [new Issue](https://github.com/backstage/backstage/issues/new?template=plugin_template.md). This helps the community know what plugins are in development.
|
||||
|
||||
You can also use this process if you have an idea for a good plugin but you hope that someone else will pick up the work.
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
# @backstage/plugin-api-docs
|
||||
|
||||
## 0.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 28edd7d29: Create backend plugin through CLI
|
||||
- 339668995: There were some missing features and markdown was not rendered properly, but this is fixed now.
|
||||
|
||||
Details:
|
||||
|
||||
- [`asyncapi/asyncapi-react#149`](https://github.com/asyncapi/asyncapi-react/pull/149) - fix: improve markdown rendering of nested fields
|
||||
- [`asyncapi/asyncapi-react#150`](https://github.com/asyncapi/asyncapi-react/pull/150) - feat: display the description of channels and operations
|
||||
- [`asyncapi/asyncapi-react#153`](https://github.com/asyncapi/asyncapi-react/pull/153) - fix: let the list of `enums` break into multiple lines
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- a73979d45: Resolve some dark mode styling issues in asyncAPI specs
|
||||
- Updated dependencies [28edd7d29]
|
||||
- Updated dependencies [819a70229]
|
||||
- Updated dependencies [3a4236570]
|
||||
- Updated dependencies [ae5983387]
|
||||
- Updated dependencies [0d4459c08]
|
||||
- Updated dependencies [482b6313d]
|
||||
- Updated dependencies [e0be86b6f]
|
||||
- Updated dependencies [f70a52868]
|
||||
- Updated dependencies [12b5fe940]
|
||||
- Updated dependencies [368fd8243]
|
||||
- Updated dependencies [1c60f716e]
|
||||
- Updated dependencies [144c66d50]
|
||||
- Updated dependencies [a768a07fb]
|
||||
- Updated dependencies [b79017fd3]
|
||||
- Updated dependencies [6d97d2d6f]
|
||||
- Updated dependencies [5adfc005e]
|
||||
- Updated dependencies [f0aa01bcc]
|
||||
- Updated dependencies [0aecfded0]
|
||||
- Updated dependencies [93a3fa3ae]
|
||||
- Updated dependencies [782f3b354]
|
||||
- Updated dependencies [8b9c8196f]
|
||||
- Updated dependencies [2713f28f4]
|
||||
- Updated dependencies [406015b0d]
|
||||
- Updated dependencies [82759d3e4]
|
||||
- Updated dependencies [60d40892c]
|
||||
- Updated dependencies [ac8d5d5c7]
|
||||
- Updated dependencies [2ebcfac8d]
|
||||
- Updated dependencies [fa56f4615]
|
||||
- Updated dependencies [ebca83d48]
|
||||
- Updated dependencies [aca79334f]
|
||||
- Updated dependencies [c0d5242a0]
|
||||
- Updated dependencies [b3d57961c]
|
||||
- Updated dependencies [0b956f21b]
|
||||
- Updated dependencies [97c2cb19b]
|
||||
- Updated dependencies [3beb5c9fc]
|
||||
- Updated dependencies [754e31db5]
|
||||
- Updated dependencies [1611c6dbc]
|
||||
- @backstage/plugin-catalog@0.2.0
|
||||
- @backstage/core@0.2.0
|
||||
- @backstage/catalog-model@0.2.0
|
||||
- @backstage/theme@0.2.0
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-api-docs",
|
||||
"version": "0.1.1-alpha.26",
|
||||
"version": "0.2.0",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -20,11 +20,11 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/catalog-model": "^0.1.1-alpha.26",
|
||||
"@backstage/core": "^0.1.1-alpha.26",
|
||||
"@backstage/plugin-catalog": "^0.1.1-alpha.26",
|
||||
"@backstage/theme": "^0.1.1-alpha.26",
|
||||
"@kyma-project/asyncapi-react": "^0.13.1",
|
||||
"@backstage/catalog-model": "^0.2.0",
|
||||
"@backstage/core": "^0.2.0",
|
||||
"@backstage/plugin-catalog": "^0.2.0",
|
||||
"@backstage/theme": "^0.2.0",
|
||||
"@kyma-project/asyncapi-react": "^0.14.2",
|
||||
"@material-icons/font": "^1.0.2",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
@@ -39,9 +39,9 @@
|
||||
"swagger-ui-react": "^3.31.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.26",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.26",
|
||||
"@backstage/test-utils": "^0.1.1-alpha.26",
|
||||
"@backstage/cli": "^0.2.0",
|
||||
"@backstage/dev-utils": "^0.1.2",
|
||||
"@backstage/test-utils": "^0.1.2",
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^10.4.1",
|
||||
"@testing-library/user-event": "^12.0.7",
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# @backstage/plugin-app-backend
|
||||
|
||||
## 0.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 28edd7d29: Create backend plugin through CLI
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [5249594c5]
|
||||
- Updated dependencies [56e4eb589]
|
||||
- Updated dependencies [e37c0a005]
|
||||
- Updated dependencies [f00ca3cb8]
|
||||
- Updated dependencies [6579769df]
|
||||
- Updated dependencies [8c2b76e45]
|
||||
- Updated dependencies [440a17b39]
|
||||
- Updated dependencies [8afce088a]
|
||||
- Updated dependencies [ce5512bc0]
|
||||
- Updated dependencies [7bbeb049f]
|
||||
- @backstage/backend-common@0.2.0
|
||||
- @backstage/config-loader@0.2.0
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-app-backend",
|
||||
"version": "0.1.1-alpha.26",
|
||||
"version": "0.2.0",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -20,8 +20,8 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-common": "^0.1.1-alpha.26",
|
||||
"@backstage/config-loader": "^0.1.1-alpha.26",
|
||||
"@backstage/backend-common": "^0.2.0",
|
||||
"@backstage/config-loader": "^0.2.0",
|
||||
"@types/express": "^4.17.6",
|
||||
"express": "^4.17.1",
|
||||
"express-promise-router": "^3.0.3",
|
||||
@@ -30,7 +30,7 @@
|
||||
"yn": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.26",
|
||||
"@backstage/cli": "^0.2.0",
|
||||
"@types/supertest": "^2.0.8",
|
||||
"msw": "^0.20.5",
|
||||
"supertest": "^4.0.2"
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
# @backstage/plugin-auth-backend
|
||||
|
||||
## 0.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 28edd7d29: Create backend plugin through CLI
|
||||
- 819a70229: Add SAML login to backstage
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
- 6d29605db: Change the default backend plugin mount point to /api
|
||||
- 5249594c5: Add service discovery interface and implement for single host deployments
|
||||
|
||||
Fixes #1847, #2596
|
||||
|
||||
Went with an interface similar to the frontend DiscoveryApi, since it's dead simple but still provides a lot of flexibility in the implementation.
|
||||
|
||||
Also ended up with two different methods, one for internal endpoint discovery and one for external. The two use-cases are explained a bit more in the docs, but basically it's service-to-service vs callback URLs.
|
||||
|
||||
This did get me thinking about uniqueness and that we're heading towards a global namespace for backend plugin IDs. That's probably fine, but if we're happy with that we should leverage it a bit more to simplify the backend setup. For example we'd have each plugin provide its own ID and not manually mount on paths in the backend.
|
||||
|
||||
Draft until we're happy with the implementation, then I can add more docs and changelog entry. Also didn't go on a thorough hunt for places where discovery can be used, but I don't think there are many since it's been pretty awkward to do service-to-service communication.
|
||||
|
||||
- 6f1768c0f: Initial implementation of catalog user lookup
|
||||
|
||||
This adds a basic catalog client + method for the Google provider to look up users in the catalog. It expects to find a single user entity in the catalog with a google.com/email annotation that matches the email of the Google profile.
|
||||
|
||||
Right now it falls back to the old behavior of splitting the email, since I don't wanna break the sign-in flow for existing apps, not yet anyway x).
|
||||
|
||||
- Added "@backstage/catalog-model@^0.1.1-alpha.23" as a dependency
|
||||
- Added "node-fetch@^2.6.1" as a dependency
|
||||
|
||||
- 1687b8fbb: Lookup user in Google Auth Provider
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- b4e5466e1: Move auth provider router creation to router
|
||||
- b652bf2cc: Add OneLogin Identity Provider to Auth Backend
|
||||
- e142a2767: Better presentation of authentication errors
|
||||
- Updated dependencies [3a4236570]
|
||||
- Updated dependencies [e0be86b6f]
|
||||
- Updated dependencies [f70a52868]
|
||||
- Updated dependencies [12b5fe940]
|
||||
- Updated dependencies [5249594c5]
|
||||
- Updated dependencies [56e4eb589]
|
||||
- Updated dependencies [e37c0a005]
|
||||
- Updated dependencies [a768a07fb]
|
||||
- Updated dependencies [f00ca3cb8]
|
||||
- Updated dependencies [6579769df]
|
||||
- Updated dependencies [5adfc005e]
|
||||
- Updated dependencies [8c2b76e45]
|
||||
- Updated dependencies [440a17b39]
|
||||
- Updated dependencies [fa56f4615]
|
||||
- Updated dependencies [8afce088a]
|
||||
- Updated dependencies [b3d57961c]
|
||||
- Updated dependencies [7bbeb049f]
|
||||
- @backstage/catalog-model@0.2.0
|
||||
- @backstage/backend-common@0.2.0
|
||||
@@ -52,9 +52,38 @@ export AUTH_GITHUB_ENTERPRISE_INSTANCE_URL=https://x
|
||||
|
||||
### Gitlab
|
||||
|
||||
#### Creating a GitLab OAuth application
|
||||
|
||||
Follow this link, [Add new application](https://gitlab.com/-/profile/applications).
|
||||
|
||||
1. Set Application Name to `backstage-dev` or something along those lines.
|
||||
1. The Authorization Callback URL should match the redirect URI set in Backstage.
|
||||
1. Set this to `http://localhost:7000/api/auth/gitlab/handler/frame` for local development.
|
||||
1. Set this to `http://{APP_FQDN}:{APP_BACKEND_PORT}/api/auth/gitlab/handler/frame` for non-local deployments.
|
||||
1. Select the following scopes from the list:
|
||||
- [x] `read_user` Grants read-only access to the authenticated user's profile through the /user API endpoint, which includes username, public email, and full name. Also grants access to read-only API endpoints under /users.
|
||||
- [x] `read_repository` Grants read-only access to repositories on private projects using Git-over-HTTP (not using the API).
|
||||
- [x] `write_repository` Grants read-write access to repositories on private projects using Git-over-HTTP (not using the API).
|
||||
- [x] `openid` Grants permission to authenticate with GitLab using OpenID Connect. Also gives read-only access to the user's profile and group memberships.
|
||||
- [x] `profile` Grants read-only access to the user's profile data using OpenID Connect.
|
||||
- [x] `email` Grants read-only access to the user's primary email address using OpenID Connect.
|
||||
|
||||
```bash
|
||||
export GITLAB_BASE_URL=x # default is https://gitlab.com
|
||||
export AUTH_GITLAB_CLIENT_ID=x
|
||||
export GITLAB_BASE_URL=https://gitlab.com
|
||||
export AUTH_GITLAB_CLIENT_ID=x # Gitlab calls this the Application ID
|
||||
export AUTH_GITLAB_CLIENT_SECRET=x
|
||||
```
|
||||
|
||||
#### Creating a GitLab Enterprise OAuth application
|
||||
|
||||
If you have GitLab Enterprise, perform the steps above, replacing `gitlab.example.com` in the following
|
||||
with the domain of your GitLab Enterprise host:
|
||||
|
||||
Create new application at: `https://gitlab.example.com/profile/applications`.
|
||||
|
||||
```bash
|
||||
export GITLAB_BASE_URL=https://gitlab.example.com
|
||||
export AUTH_GITLAB_CLIENT_ID=x # GitLab calls this the Application ID
|
||||
export AUTH_GITLAB_CLIENT_SECRET=x
|
||||
```
|
||||
|
||||
@@ -119,7 +148,7 @@ To try out SAML, you can use the mock identity provider:
|
||||
|
||||
## Authentication providers
|
||||
|
||||
[How to add an auth provider](https://github.com/spotify/backstage/blob/master/docs/auth/add-auth-provider.md)
|
||||
[How to add an auth provider](https://github.com/backstage/backstage/blob/master/docs/auth/add-auth-provider.md)
|
||||
|
||||
## Links
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-auth-backend",
|
||||
"version": "0.1.1-alpha.26",
|
||||
"version": "0.2.0",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -20,9 +20,9 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-common": "^0.1.1-alpha.26",
|
||||
"@backstage/catalog-model": "^0.1.1-alpha.26",
|
||||
"@backstage/config": "^0.1.1-alpha.26",
|
||||
"@backstage/backend-common": "^0.2.0",
|
||||
"@backstage/catalog-model": "^0.2.0",
|
||||
"@backstage/config": "^0.1.1",
|
||||
"@types/express": "^4.17.6",
|
||||
"compression": "^1.7.4",
|
||||
"cookie-parser": "^1.4.5",
|
||||
@@ -45,13 +45,14 @@
|
||||
"passport-microsoft": "^0.1.0",
|
||||
"passport-oauth2": "^1.5.0",
|
||||
"passport-okta-oauth": "^0.0.1",
|
||||
"passport-onelogin-oauth": "^0.0.1",
|
||||
"passport-saml": "^1.3.3",
|
||||
"uuid": "^8.0.0",
|
||||
"winston": "^3.2.1",
|
||||
"yn": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.26",
|
||||
"@backstage/cli": "^0.2.0",
|
||||
"@types/body-parser": "^1.19.0",
|
||||
"@types/cookie-parser": "^1.4.2",
|
||||
"@types/jwt-decode": "2.2.1",
|
||||
|
||||
@@ -116,7 +116,6 @@ export const executeFrameHandlerStrategy = async <T, PrivateInfo = never>(
|
||||
strategy.redirect = () => {
|
||||
reject(new Error('Unexpected redirect'));
|
||||
};
|
||||
|
||||
strategy.authenticate(req, {});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -22,6 +22,7 @@ import { createOktaProvider } from './okta';
|
||||
import { createSamlProvider } from './saml';
|
||||
import { createAuth0Provider } from './auth0';
|
||||
import { createMicrosoftProvider } from './microsoft';
|
||||
import { createOneLoginProvider } from './onelogin';
|
||||
import { AuthProviderFactory, AuthProviderFactoryOptions } from './types';
|
||||
|
||||
const factories: { [providerId: string]: AuthProviderFactory } = {
|
||||
@@ -33,6 +34,7 @@ const factories: { [providerId: string]: AuthProviderFactory } = {
|
||||
auth0: createAuth0Provider,
|
||||
microsoft: createMicrosoftProvider,
|
||||
oauth2: createOAuth2Provider,
|
||||
onelogin: createOneLoginProvider,
|
||||
};
|
||||
|
||||
export function createAuthProvider(
|
||||
|
||||
@@ -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 { createOneLoginProvider } from './provider';
|
||||
@@ -0,0 +1,173 @@
|
||||
/*
|
||||
* 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 { Strategy as OneLoginStrategy } from 'passport-onelogin-oauth';
|
||||
import express from 'express';
|
||||
import {
|
||||
OAuthAdapter,
|
||||
OAuthProviderOptions,
|
||||
OAuthHandlers,
|
||||
OAuthResponse,
|
||||
OAuthEnvironmentHandler,
|
||||
OAuthStartRequest,
|
||||
encodeState,
|
||||
OAuthRefreshRequest,
|
||||
} from '../../lib/oauth';
|
||||
import passport from 'passport';
|
||||
import {
|
||||
executeFrameHandlerStrategy,
|
||||
executeRedirectStrategy,
|
||||
executeRefreshTokenStrategy,
|
||||
makeProfileInfo,
|
||||
executeFetchUserProfileStrategy,
|
||||
PassportDoneCallback,
|
||||
} from '../../lib/passport';
|
||||
import { RedirectInfo, AuthProviderFactory } from '../types';
|
||||
|
||||
type PrivateInfo = {
|
||||
refreshToken: string;
|
||||
};
|
||||
|
||||
export type OneLoginProviderOptions = OAuthProviderOptions & {
|
||||
issuer: string;
|
||||
};
|
||||
|
||||
export class OneLoginProvider implements OAuthHandlers {
|
||||
private readonly _strategy: any;
|
||||
|
||||
constructor(options: OneLoginProviderOptions) {
|
||||
this._strategy = new OneLoginStrategy(
|
||||
{
|
||||
issuer: options.issuer,
|
||||
clientID: options.clientId,
|
||||
clientSecret: options.clientSecret,
|
||||
callbackURL: options.callbackUrl,
|
||||
passReqToCallback: false as true,
|
||||
},
|
||||
(
|
||||
accessToken: any,
|
||||
refreshToken: any,
|
||||
params: any,
|
||||
rawProfile: passport.Profile,
|
||||
done: PassportDoneCallback<OAuthResponse, PrivateInfo>,
|
||||
) => {
|
||||
const profile = makeProfileInfo(rawProfile, params.id_token);
|
||||
|
||||
done(
|
||||
undefined,
|
||||
{
|
||||
providerInfo: {
|
||||
idToken: params.id_token,
|
||||
accessToken,
|
||||
scope: params.scope,
|
||||
expiresInSeconds: params.expires_in,
|
||||
},
|
||||
profile,
|
||||
},
|
||||
{
|
||||
refreshToken,
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
async start(req: OAuthStartRequest): Promise<RedirectInfo> {
|
||||
return await executeRedirectStrategy(req, this._strategy, {
|
||||
accessType: 'offline',
|
||||
prompt: 'consent',
|
||||
scope: 'openid',
|
||||
state: encodeState(req.state),
|
||||
});
|
||||
}
|
||||
|
||||
async handler(
|
||||
req: express.Request,
|
||||
): Promise<{ response: OAuthResponse; refreshToken: string }> {
|
||||
const { response, privateInfo } = await executeFrameHandlerStrategy<
|
||||
OAuthResponse,
|
||||
PrivateInfo
|
||||
>(req, this._strategy);
|
||||
|
||||
return {
|
||||
response: await this.populateIdentity(response),
|
||||
refreshToken: privateInfo.refreshToken,
|
||||
};
|
||||
}
|
||||
|
||||
async refresh(req: OAuthRefreshRequest): Promise<OAuthResponse> {
|
||||
const { accessToken, params } = await executeRefreshTokenStrategy(
|
||||
this._strategy,
|
||||
req.refreshToken,
|
||||
req.scope,
|
||||
);
|
||||
|
||||
const profile = await executeFetchUserProfileStrategy(
|
||||
this._strategy,
|
||||
accessToken,
|
||||
params.id_token,
|
||||
);
|
||||
|
||||
return this.populateIdentity({
|
||||
providerInfo: {
|
||||
accessToken,
|
||||
idToken: params.id_token,
|
||||
expiresInSeconds: params.expires_in,
|
||||
scope: params.scope,
|
||||
},
|
||||
profile,
|
||||
});
|
||||
}
|
||||
|
||||
private async populateIdentity(
|
||||
response: OAuthResponse,
|
||||
): Promise<OAuthResponse> {
|
||||
const { profile } = response;
|
||||
|
||||
if (!profile.email) {
|
||||
throw new Error('OIDC profile contained no email');
|
||||
}
|
||||
|
||||
const id = profile.email.split('@')[0];
|
||||
|
||||
return { ...response, backstageIdentity: { id } };
|
||||
}
|
||||
}
|
||||
|
||||
export const createOneLoginProvider: AuthProviderFactory = ({
|
||||
globalConfig,
|
||||
config,
|
||||
tokenIssuer,
|
||||
}) =>
|
||||
OAuthEnvironmentHandler.mapConfig(config, envConfig => {
|
||||
const providerId = 'onelogin';
|
||||
const clientId = envConfig.getString('clientId');
|
||||
const clientSecret = envConfig.getString('clientSecret');
|
||||
const issuer = envConfig.getString('issuer');
|
||||
const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`;
|
||||
|
||||
const provider = new OneLoginProvider({
|
||||
clientId,
|
||||
clientSecret,
|
||||
callbackUrl,
|
||||
issuer,
|
||||
});
|
||||
|
||||
return OAuthAdapter.fromConfig(globalConfig, provider, {
|
||||
disableRefresh: false,
|
||||
providerId,
|
||||
tokenIssuer,
|
||||
});
|
||||
});
|
||||
@@ -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.
|
||||
*/
|
||||
declare module 'passport-onelogin-oauth' {
|
||||
export class Strategy {
|
||||
constructor(options: any, verify: any);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
# @backstage/plugin-catalog-backend
|
||||
|
||||
## 0.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- e0be86b6f: Entirely case insensitive read path of entities
|
||||
- 12b5fe940: Add ApiDefinitionAtLocationProcessor that allows to load a API definition from another location
|
||||
- 57d555eb2: This feature works the same as \$secret does in config - it allows programmatic substitution of values into a document.
|
||||
|
||||
This is particularly useful e.g. for API type entities where you do not want to repeat your entire API spec document inside the catalog-info.yaml file. For those cases, you can instead do something like
|
||||
|
||||
```
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
kind: API
|
||||
metadata:
|
||||
name: my-federated-service
|
||||
spec:
|
||||
type: graphql
|
||||
definition:
|
||||
$text: ./schema.graphql
|
||||
```
|
||||
|
||||
The textual content of that file will be injected as the value of definition, during each refresh loop. Both relative and absolute paths are supported, as well as any HTTP/HTTPS URL pointing to a service that returns the relevant data.
|
||||
|
||||
The initial version supports injection of text file data, and structured data from JSON and YAML files. You can add any handler of your own in addition to these.
|
||||
|
||||
- 61db1ddc6: Allow node v14 and add to master build matrix
|
||||
|
||||
- Upgrade sqlite3@^5.0.0 in @backstage/plugin-catalog-backend
|
||||
- Add Node 14 to engines in @backstage/create-app
|
||||
|
||||
- 81cb94379: Simplify the read function in processors
|
||||
- a768a07fb: Add the ability to import users from GitHub Organization into the catalog.
|
||||
|
||||
The token needs to have the scopes `user:email`, `read:user`, and `read:org`.
|
||||
|
||||
- ce1f55398: Use the new `UrlReader` in `PlaceholderProcessor`.
|
||||
This allows to use the placeholder processor to include API definitions in API entities.
|
||||
Previously it was only possible to do this if the definition comes from the same location type as the entity itself.
|
||||
- e6b00e3af: Remove the backstage.io/definition-at-location annotation.
|
||||
The annotation was superseded by the placeholder processor.
|
||||
|
||||
```yaml
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
kind: API
|
||||
metadata:
|
||||
name: spotify
|
||||
description: The Spotify web API
|
||||
tags:
|
||||
- spotify
|
||||
- rest
|
||||
annotations:
|
||||
# Don't use this annotation, but the placeholder $text instead (see below).
|
||||
backstage.io/definition-at-location: 'url:https://raw.githubusercontent.com/APIs-guru/openapi-directory/master/APIs/spotify.com/v1/swagger.yaml'
|
||||
spec:
|
||||
type: openapi
|
||||
lifecycle: production
|
||||
owner: spotify@example.com
|
||||
definition:
|
||||
$text: https://raw.githubusercontent.com/APIs-guru/openapi-directory/master/APIs/spotify.com/v1/swagger.yaml
|
||||
```
|
||||
|
||||
- 99710b102: The way that wiring together a catalog happens, has changed drastically. Now
|
||||
there is a new class `CatalogBuilder` that does almost all of the heavy lifting
|
||||
of how to augment/replace pieces of catalog functionality, such as adding
|
||||
support for custom entities or adding additional processors.
|
||||
|
||||
As the builder was added, a lot of the static methods and builders for default
|
||||
setups have been removed from classes deep in the hierarchy. Instead, the
|
||||
builder contains the knowledge of what the defaults are.
|
||||
|
||||
- 002860e7a: Filters passed to the `/entities` endpoint of the catalog has changed format.
|
||||
|
||||
The old way was to pass things on the form `?a=b&c=d`; the new way is to pass
|
||||
things on the form `?filter=a=b,c=d`. See discussion in
|
||||
[#2910](https://github.com/backstage/backstage/issues/2910) for details.
|
||||
|
||||
The comma separated items within a single filter have an AND between them. If
|
||||
multiple such filters are passed, they have an OR between those item groups.
|
||||
|
||||
- 5adfc005e: Changes the various kind policies into a new type `KindValidator`.
|
||||
|
||||
Adds `CatalogProcessor#validateEntityKind` that makes use of the above
|
||||
validators. This moves entity schema validity checking away from entity
|
||||
policies and into processors, centralizing the extension points into the
|
||||
processor chain.
|
||||
|
||||
- 948052cbb: Add ability to dry run adding a new location to the catalog API.
|
||||
|
||||
The location is now added in a transaction and afterwards rolled back.
|
||||
This allows users to dry run this operation to see if there entity has issues.
|
||||
This is probably done by automated tools in the CI/CD pipeline.
|
||||
|
||||
- 4036ff59d: - The `CatalogProcessor` API was updated to have `preProcessEntity` and
|
||||
`postProcessEntity` methods, instead of just one `processEntity`. This makes
|
||||
it easier to make processors that have several stages in one, and to make
|
||||
different processors more position independent in the list of processors.
|
||||
- The `EntityPolicy` is now given directly to the `LocationReaders`, instead of
|
||||
being enforced inside a policy. We have decided to separate out the act of
|
||||
validating an entity to be outside of the processing flow, to make it
|
||||
possible to apply more liberally and to evolve it as a separate concept.
|
||||
- Because of the above, the `EntityPolicyProcessor` has been removed.
|
||||
- 512d70973: Use the new `UrlReader` in the `CodeOwnersProcessor`.
|
||||
- 2f62e1804: Removed the parseData step from catalog processors. Locations readers should emit full entities instead.
|
||||
- 36a71d278: Removed support for deprecated `catalog.providers` config that have been moved to `integrations`
|
||||
- a5cb46bac: Renamed the `LocationProcessor` class to `CatalogProcessor`.
|
||||
|
||||
Likewise, renamed `LocationProcessorResult`, `LocationProcessorLocationResult`,
|
||||
`LocationProcessorDataResult`, `LocationProcessorEntityResult`,
|
||||
`LocationProcessorErrorResult`, and `LocationProcessorEmit` to their `Catalog*`
|
||||
counterparts.
|
||||
|
||||
- 49d70ccab: Remove the `read` argument of `LocationProcessor.processEntity`.
|
||||
Instead, pass the `UrlReader` into the constructor of your `LocationProcessor`.
|
||||
- 440a17b39: The catalog backend UrlReaderProcessor now uses a UrlReader from @backstage/backend-common, which must now be supplied to the constructor.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 3472c8be7: Add codeowners processor
|
||||
|
||||
- Add `codeowners-utils@^1.0.2` as a dependency
|
||||
- Add `core-js@^3.6.5` as a dependency
|
||||
- Added new CodeOwnersProcessor
|
||||
|
||||
- 33454c0f2: Fix `CatalogBuilder#addProcessor`.
|
||||
- 183e2a30d: Add support for `fields` sub-selection of just parts of an entity when listing
|
||||
entities in the catalog backend.
|
||||
|
||||
Example: `.../entities?fields=metadata.name,spec.type` will return partial
|
||||
entity objects with only those exact fields present and the rest cut out.
|
||||
Fields do not have to be simple scalars - you can for example do
|
||||
`fields=metadata`.
|
||||
|
||||
- 8bdf0bcf5: Fix CodeOwnersProcessor to handle non team users
|
||||
- 4c4eab81b: The CodeOwnersProcessor now handles 'url' locations
|
||||
- Updated dependencies [3a4236570]
|
||||
- Updated dependencies [e0be86b6f]
|
||||
- Updated dependencies [f70a52868]
|
||||
- Updated dependencies [12b5fe940]
|
||||
- Updated dependencies [5249594c5]
|
||||
- Updated dependencies [56e4eb589]
|
||||
- Updated dependencies [e37c0a005]
|
||||
- Updated dependencies [a768a07fb]
|
||||
- Updated dependencies [f00ca3cb8]
|
||||
- Updated dependencies [6579769df]
|
||||
- Updated dependencies [5adfc005e]
|
||||
- Updated dependencies [8c2b76e45]
|
||||
- Updated dependencies [440a17b39]
|
||||
- Updated dependencies [fa56f4615]
|
||||
- Updated dependencies [8afce088a]
|
||||
- Updated dependencies [b3d57961c]
|
||||
- Updated dependencies [7bbeb049f]
|
||||
- @backstage/catalog-model@0.2.0
|
||||
- @backstage/backend-common@0.2.0
|
||||
@@ -27,5 +27,5 @@ This will launch the full example backend, populated some example entities.
|
||||
|
||||
## Links
|
||||
|
||||
- [Frontend part of the plugin](https://github.com/spotify/backstage/tree/master/plugins/catalog)
|
||||
- [Frontend part of the plugin](https://github.com/backstage/backstage/tree/master/plugins/catalog)
|
||||
- [The Backstage homepage](https://backstage.io)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-catalog-backend",
|
||||
"version": "0.1.1-alpha.26",
|
||||
"version": "0.2.0",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -20,9 +20,9 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-common": "^0.1.1-alpha.26",
|
||||
"@backstage/catalog-model": "^0.1.1-alpha.26",
|
||||
"@backstage/config": "^0.1.1-alpha.26",
|
||||
"@backstage/backend-common": "^0.2.0",
|
||||
"@backstage/catalog-model": "^0.2.0",
|
||||
"@backstage/config": "^0.1.1",
|
||||
"@octokit/graphql": "^4.5.6",
|
||||
"@types/express": "^4.17.6",
|
||||
"codeowners-utils": "^1.0.2",
|
||||
@@ -45,8 +45,8 @@
|
||||
"yup": "^0.29.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.26",
|
||||
"@backstage/test-utils": "^0.1.1-alpha.26",
|
||||
"@backstage/cli": "^0.2.0",
|
||||
"@backstage/test-utils": "^0.1.2",
|
||||
"@types/core-js": "^2.5.4",
|
||||
"@types/git-url-parse": "^9.0.0",
|
||||
"@types/ldapjs": "^1.0.9",
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import type { Entity } from '@backstage/catalog-model';
|
||||
import { Database, DatabaseManager, Transaction } from '../database';
|
||||
import { EntityFilters } from '../service/EntityFilters';
|
||||
import { DatabaseEntitiesCatalog } from './DatabaseEntitiesCatalog';
|
||||
import { EntityUpsertRequest } from './types';
|
||||
|
||||
@@ -73,13 +74,12 @@ describe('DatabaseEntitiesCatalog', () => {
|
||||
]);
|
||||
|
||||
expect(db.entities).toHaveBeenCalledTimes(1);
|
||||
expect(db.entities).toHaveBeenCalledWith(expect.anything(), [
|
||||
{
|
||||
kind: 'b',
|
||||
'metadata.namespace': 'd',
|
||||
'metadata.name': ['c'],
|
||||
},
|
||||
]);
|
||||
expect(db.entities).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
EntityFilters.ofFilterString(
|
||||
'kind=b,metadata.namespace=d,metadata.name=c',
|
||||
),
|
||||
);
|
||||
expect(db.setRelations).toHaveBeenCalledTimes(1);
|
||||
expect(db.setRelations).toHaveBeenCalledWith(expect.anything(), 'u', []);
|
||||
expect(db.addEntities).toHaveBeenCalledTimes(1);
|
||||
@@ -107,13 +107,12 @@ describe('DatabaseEntitiesCatalog', () => {
|
||||
);
|
||||
|
||||
expect(db.entities).toHaveBeenCalledTimes(1);
|
||||
expect(db.entities).toHaveBeenCalledWith(expect.anything(), [
|
||||
{
|
||||
kind: 'b',
|
||||
'metadata.namespace': 'd',
|
||||
'metadata.name': ['c'],
|
||||
},
|
||||
]);
|
||||
expect(db.entities).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
EntityFilters.ofFilterString(
|
||||
'kind=b,metadata.namespace=d,metadata.name=c',
|
||||
),
|
||||
);
|
||||
expect(db.setRelations).toHaveBeenCalledTimes(1);
|
||||
expect(db.setRelations).toHaveBeenCalledWith(expect.anything(), 'u', []);
|
||||
expect(db.addEntities).toHaveBeenCalledTimes(1);
|
||||
@@ -204,13 +203,12 @@ describe('DatabaseEntitiesCatalog', () => {
|
||||
]);
|
||||
|
||||
expect(db.entities).toHaveBeenCalledTimes(1);
|
||||
expect(db.entities).toHaveBeenCalledWith(expect.anything(), [
|
||||
{
|
||||
kind: 'b',
|
||||
'metadata.namespace': 'd',
|
||||
'metadata.name': ['c'],
|
||||
},
|
||||
]);
|
||||
expect(db.entities).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
EntityFilters.ofFilterString(
|
||||
'kind=b,metadata.namespace=d,metadata.name=c',
|
||||
),
|
||||
);
|
||||
expect(db.entityByName).not.toHaveBeenCalled();
|
||||
expect(db.entityByUid).toHaveBeenCalledTimes(1);
|
||||
expect(db.entityByUid).toHaveBeenCalledWith(transaction, 'u');
|
||||
@@ -280,13 +278,12 @@ describe('DatabaseEntitiesCatalog', () => {
|
||||
]);
|
||||
|
||||
expect(db.entities).toHaveBeenCalledTimes(1);
|
||||
expect(db.entities).toHaveBeenCalledWith(expect.anything(), [
|
||||
{
|
||||
kind: 'b',
|
||||
'metadata.namespace': 'd',
|
||||
'metadata.name': ['c'],
|
||||
},
|
||||
]);
|
||||
expect(db.entities).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
EntityFilters.ofFilterString(
|
||||
'kind=b,metadata.namespace=d,metadata.name=c',
|
||||
),
|
||||
);
|
||||
expect(db.entityByName).toHaveBeenCalledTimes(1);
|
||||
expect(db.entityByName).toHaveBeenCalledWith(transaction, {
|
||||
kind: 'b',
|
||||
@@ -342,13 +339,12 @@ describe('DatabaseEntitiesCatalog', () => {
|
||||
]);
|
||||
|
||||
expect(db.entities).toHaveBeenCalledTimes(1);
|
||||
expect(db.entities).toHaveBeenCalledWith(expect.anything(), [
|
||||
{
|
||||
kind: 'b',
|
||||
'metadata.namespace': 'd',
|
||||
'metadata.name': ['c'],
|
||||
},
|
||||
]);
|
||||
expect(db.entities).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
EntityFilters.ofFilterString(
|
||||
'kind=b,metadata.namespace=d,metadata.name=c',
|
||||
),
|
||||
);
|
||||
expect(db.entityByName).not.toHaveBeenCalled();
|
||||
expect(db.entityByUid).not.toHaveBeenCalled();
|
||||
expect(db.updateEntity).not.toHaveBeenCalled();
|
||||
|
||||
@@ -16,13 +16,13 @@
|
||||
|
||||
import { ConflictError, NotFoundError } from '@backstage/backend-common';
|
||||
import {
|
||||
Entity,
|
||||
entityHasChanges,
|
||||
EntityRelationSpec,
|
||||
generateUpdatedEntity,
|
||||
getEntityName,
|
||||
LOCATION_ANNOTATION,
|
||||
serializeEntityRef,
|
||||
Entity,
|
||||
EntityRelationSpec,
|
||||
} from '@backstage/catalog-model';
|
||||
import { chunk, groupBy } from 'lodash';
|
||||
import limiterFactory from 'p-limit';
|
||||
@@ -30,9 +30,10 @@ import { Logger } from 'winston';
|
||||
import type {
|
||||
Database,
|
||||
DbEntityResponse,
|
||||
EntityFilters,
|
||||
EntityFilter,
|
||||
Transaction,
|
||||
} from '../database';
|
||||
import { EntityFilters } from '../service/EntityFilters';
|
||||
import { durationText } from '../util/timing';
|
||||
import type {
|
||||
EntitiesCatalog,
|
||||
@@ -65,9 +66,9 @@ export class DatabaseEntitiesCatalog implements EntitiesCatalog {
|
||||
private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
async entities(filters?: EntityFilters[]): Promise<Entity[]> {
|
||||
async entities(filter?: EntityFilter): Promise<Entity[]> {
|
||||
const items = await this.database.transaction(tx =>
|
||||
this.database.entities(tx, filters),
|
||||
this.database.entities(tx, filter),
|
||||
);
|
||||
return items.map(i => i.entity);
|
||||
}
|
||||
@@ -113,9 +114,12 @@ export class DatabaseEntitiesCatalog implements EntitiesCatalog {
|
||||
const location =
|
||||
entityResponse.entity.metadata.annotations?.[LOCATION_ANNOTATION];
|
||||
const colocatedEntities = location
|
||||
? await this.database.entities(tx, [
|
||||
{ [`metadata.annotations.${LOCATION_ANNOTATION}`]: location },
|
||||
])
|
||||
? await this.database.entities(
|
||||
tx,
|
||||
EntityFilters.ofMatchers({
|
||||
[`metadata.annotations.${LOCATION_ANNOTATION}`]: location,
|
||||
}),
|
||||
)
|
||||
: [entityResponse];
|
||||
for (const dbResponse of colocatedEntities) {
|
||||
await this.database.removeEntityByUid(
|
||||
@@ -219,9 +223,12 @@ export class DatabaseEntitiesCatalog implements EntitiesCatalog {
|
||||
}
|
||||
|
||||
if (options?.outputEntities) {
|
||||
const writtenEntities = await this.database.entities(tx, [
|
||||
{ 'metadata.uid': modifiedEntityIds.map(e => e.entityId) },
|
||||
]);
|
||||
const writtenEntities = await this.database.entities(
|
||||
tx,
|
||||
EntityFilters.ofMatchers({
|
||||
'metadata.uid': modifiedEntityIds.map(e => e.entityId),
|
||||
}),
|
||||
);
|
||||
|
||||
modifiedEntityIds = writtenEntities.map(e => ({
|
||||
entityId: e.entity.metadata.uid!,
|
||||
@@ -241,7 +248,7 @@ export class DatabaseEntitiesCatalog implements EntitiesCatalog {
|
||||
// If this is only a dry run, cancel the database transaction even if it was successful.
|
||||
await tx.rollback();
|
||||
|
||||
this.logger.debug(`Perfomed successful dry run of adding entities`);
|
||||
this.logger.debug(`Performed successful dry run of adding entities`);
|
||||
}
|
||||
|
||||
return entityUpserts;
|
||||
@@ -273,13 +280,14 @@ export class DatabaseEntitiesCatalog implements EntitiesCatalog {
|
||||
const markTimestamp = process.hrtime();
|
||||
|
||||
const names = requests.map(({ entity }) => entity.metadata.name);
|
||||
const oldEntities = await this.database.entities(tx, [
|
||||
{
|
||||
const oldEntities = await this.database.entities(
|
||||
tx,
|
||||
EntityFilters.ofMatchers({
|
||||
kind: kind,
|
||||
'metadata.namespace': namespace,
|
||||
'metadata.name': names,
|
||||
},
|
||||
]);
|
||||
}),
|
||||
);
|
||||
|
||||
const oldEntitiesByName = new Map(
|
||||
oldEntities.map(e => [e.entity.metadata.name, e.entity]),
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import { Entity, EntityRelationSpec, Location } from '@backstage/catalog-model';
|
||||
import type { EntityFilters } from '../database';
|
||||
import type { EntityFilter } from '../database';
|
||||
|
||||
//
|
||||
// Entities
|
||||
@@ -32,7 +32,7 @@ export type EntityUpsertResponse = {
|
||||
};
|
||||
|
||||
export type EntitiesCatalog = {
|
||||
entities(filters?: EntityFilters[]): Promise<Entity[]>;
|
||||
entities(filter?: EntityFilter): Promise<Entity[]>;
|
||||
removeEntityByUid(uid: string): Promise<void>;
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
import { ConflictError } from '@backstage/backend-common';
|
||||
import { Entity, Location, parseEntityRef } from '@backstage/catalog-model';
|
||||
import { EntityFilters } from '../service/EntityFilters';
|
||||
import { DatabaseManager } from './DatabaseManager';
|
||||
import type {
|
||||
DbEntityRequest,
|
||||
@@ -348,7 +349,7 @@ describe('CommonDatabase', () => {
|
||||
await db.transaction(async tx => {
|
||||
await db.addEntities(tx, [{ entity: e1 }, { entity: e2 }]);
|
||||
});
|
||||
const result = await db.transaction(async tx => db.entities(tx, []));
|
||||
const result = await db.transaction(async tx => db.entities(tx));
|
||||
expect(result.length).toEqual(2);
|
||||
expect(result).toEqual(
|
||||
expect.arrayContaining([
|
||||
@@ -390,7 +391,7 @@ describe('CommonDatabase', () => {
|
||||
|
||||
await expect(
|
||||
db.transaction(async tx =>
|
||||
db.entities(tx, [{ kind: 'k2', 'spec.c': 'some' }]),
|
||||
db.entities(tx, EntityFilters.ofFilterString('kind=k2,spec.c=some')),
|
||||
),
|
||||
).resolves.toEqual([
|
||||
{
|
||||
@@ -400,56 +401,14 @@ describe('CommonDatabase', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('can get all specific entities for matching filters with nulls (both missing and literal null value)', async () => {
|
||||
it('can get all specific entities for matching filters case insensitively', async () => {
|
||||
const entities: Entity[] = [
|
||||
{ apiVersion: 'a', kind: 'k1', metadata: { name: 'n' } },
|
||||
{
|
||||
apiVersion: 'a',
|
||||
kind: 'k2',
|
||||
metadata: { name: 'n' },
|
||||
spec: { c: 'some' },
|
||||
apiVersion: 'A',
|
||||
kind: 'K1',
|
||||
metadata: { name: 'N' },
|
||||
spec: { c: 'SOME' },
|
||||
},
|
||||
{
|
||||
apiVersion: 'a',
|
||||
kind: 'k3',
|
||||
metadata: { name: 'n' },
|
||||
spec: { c: null },
|
||||
},
|
||||
];
|
||||
|
||||
await db.transaction(async tx => {
|
||||
await db.addEntities(
|
||||
tx,
|
||||
entities.map(entity => ({ entity })),
|
||||
);
|
||||
});
|
||||
|
||||
const rows = await db.transaction(async tx =>
|
||||
db.entities(tx, [{ apiVersion: 'a', 'spec.c': [null, 'some'] }]),
|
||||
);
|
||||
|
||||
expect(rows.length).toEqual(3);
|
||||
expect(rows).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
locationId: undefined,
|
||||
entity: expect.objectContaining({ kind: 'k1' }),
|
||||
},
|
||||
{
|
||||
locationId: undefined,
|
||||
entity: expect.objectContaining({ kind: 'k2' }),
|
||||
},
|
||||
{
|
||||
locationId: undefined,
|
||||
entity: expect.objectContaining({ kind: 'k3' }),
|
||||
},
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('can get all specific entities for matching filters case insensitively)', async () => {
|
||||
const entities: Entity[] = [
|
||||
{ apiVersion: 'A', kind: 'K1', metadata: { name: 'N' } },
|
||||
{
|
||||
apiVersion: 'a',
|
||||
kind: 'k2',
|
||||
@@ -460,7 +419,7 @@ describe('CommonDatabase', () => {
|
||||
apiVersion: 'a',
|
||||
kind: 'k3',
|
||||
metadata: { name: 'n' },
|
||||
spec: { c: null },
|
||||
spec: { c: 'somE' },
|
||||
},
|
||||
];
|
||||
|
||||
@@ -472,7 +431,10 @@ describe('CommonDatabase', () => {
|
||||
});
|
||||
|
||||
const rows = await db.transaction(async tx =>
|
||||
db.entities(tx, [{ ApiVersioN: 'A', 'spEc.C': [null, 'some'] }]),
|
||||
db.entities(
|
||||
tx,
|
||||
EntityFilters.ofFilterString('ApiVersioN=A,spEc.C=some'),
|
||||
),
|
||||
);
|
||||
|
||||
expect(rows.length).toEqual(3);
|
||||
|
||||
@@ -45,7 +45,7 @@ import {
|
||||
DbEntityResponse,
|
||||
DbLocationsRow,
|
||||
DbLocationsRowWithStatus,
|
||||
EntityFilters,
|
||||
EntityFilter,
|
||||
Transaction,
|
||||
} from './types';
|
||||
|
||||
@@ -233,71 +233,36 @@ export class CommonDatabase implements Database {
|
||||
|
||||
async entities(
|
||||
txOpaque: Transaction,
|
||||
filters?: EntityFilters[],
|
||||
filter?: EntityFilter,
|
||||
): Promise<DbEntityResponse[]> {
|
||||
const tx = txOpaque as Knex.Transaction<any, any>;
|
||||
|
||||
let entitiesQuery = tx<DbEntitiesRow>('entities');
|
||||
|
||||
for (const singleFilter of filters ?? []) {
|
||||
for (const singleFilter of filter?.anyOf ?? []) {
|
||||
entitiesQuery = entitiesQuery.orWhere(function singleFilterFn() {
|
||||
for (const [matchKey, matchVal] of Object.entries(singleFilter)) {
|
||||
const key = matchKey.toLowerCase().replace(/[*]/g, '%');
|
||||
const keyOp = key.includes('%') ? 'like' : '=';
|
||||
const values = Array.isArray(matchVal) ? matchVal : [matchVal];
|
||||
|
||||
let matchNulls = false;
|
||||
const matchIn: string[] = [];
|
||||
const matchLike: string[] = [];
|
||||
|
||||
for (const value of values) {
|
||||
if (!value) {
|
||||
matchNulls = true;
|
||||
} else if (value.includes('*')) {
|
||||
matchLike.push(value.toLowerCase().replace(/[*]/g, '%'));
|
||||
} else {
|
||||
matchIn.push(value.toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
for (const { key, matchValueIn } of singleFilter.allOf) {
|
||||
// NOTE(freben): This used to be a set of OUTER JOIN, which may seem to
|
||||
// make a lot of sense. However, it had abysmal performance on sqlite
|
||||
// when datasets grew large, so we're using IN instead.
|
||||
const matchQuery = tx<DbEntitiesSearchRow>('entities_search')
|
||||
.select('entity_id')
|
||||
.where(function keyFilter() {
|
||||
this.andWhere('key', keyOp, key);
|
||||
this.andWhere(function valueFilter() {
|
||||
if (matchIn.length === 1) {
|
||||
this.orWhere({ value: matchIn[0] });
|
||||
} else if (matchIn.length > 1) {
|
||||
this.orWhereIn('value', matchIn);
|
||||
this.andWhere({ key: key.toLowerCase() });
|
||||
if (matchValueIn) {
|
||||
if (matchValueIn.length === 1) {
|
||||
this.andWhere({ value: matchValueIn[0].toLowerCase() });
|
||||
} else if (matchValueIn.length > 1) {
|
||||
this.andWhere(
|
||||
'value',
|
||||
'in',
|
||||
matchValueIn.map(v => v.toLowerCase()),
|
||||
);
|
||||
}
|
||||
if (matchLike.length) {
|
||||
for (const x of matchLike) {
|
||||
this.orWhere('value', 'like', tx.raw('?', [x]));
|
||||
}
|
||||
}
|
||||
if (matchNulls) {
|
||||
// Match explicit nulls, and then handle absence separately
|
||||
// below
|
||||
this.orWhereNull('value');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Handle absence as nulls as well
|
||||
this.andWhere(function match() {
|
||||
this.whereIn('id', matchQuery);
|
||||
if (matchNulls) {
|
||||
this.orWhereNotIn(
|
||||
'id',
|
||||
tx<DbEntitiesSearchRow>('entities_search')
|
||||
.select('entity_id')
|
||||
.where('key', keyOp, key),
|
||||
);
|
||||
}
|
||||
});
|
||||
this.andWhere('id', 'in', matchQuery);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ export type {
|
||||
Database,
|
||||
DbEntityRequest,
|
||||
DbEntityResponse,
|
||||
EntitiesSearchFilter,
|
||||
EntityFilter,
|
||||
EntityFilters,
|
||||
Transaction,
|
||||
} from './types';
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
import type {
|
||||
Entity,
|
||||
EntityName,
|
||||
Location,
|
||||
EntityRelationSpec,
|
||||
Location,
|
||||
} from '@backstage/catalog-model';
|
||||
|
||||
export type DbEntitiesRow = {
|
||||
@@ -80,26 +80,35 @@ export type DatabaseLocationUpdateLogEvent = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Filter matcher for a single entity field.
|
||||
*
|
||||
* Can be either null or a string, or an array of those. Null and the empty
|
||||
* string are treated equally, and match both a present field with a null or
|
||||
* empty value, as well as an absent field.
|
||||
*
|
||||
* A filter may contain asterisks (*) that are treated as wildcards for zero
|
||||
* or more arbitrary characters.
|
||||
* Matches rows in the entities_search table.
|
||||
*/
|
||||
export type EntityFilter = null | string | (null | string)[];
|
||||
export type EntitiesSearchFilter = {
|
||||
/**
|
||||
* The key to match on.
|
||||
*
|
||||
* Matches are always case insensitive.
|
||||
*/
|
||||
key: string;
|
||||
|
||||
/**
|
||||
* Match on plain equality of values.
|
||||
*
|
||||
* If undefined, this factor is not taken into account. Otherwise, match on
|
||||
* values that are equal to any of the given array items. Matches are always
|
||||
* case insensitive.
|
||||
*/
|
||||
matchValueIn?: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* A set of filter matchers used for filtering entities.
|
||||
* A filter expression for entities.
|
||||
*
|
||||
* The keys are full dot-separated paths into the structure of an entity, for
|
||||
* example "metadata.name". You can also address any item in an array the same
|
||||
* way, e.g. "a.b.c": "x" works if b is an array of objects that have a c field
|
||||
* and any of those have the value x.
|
||||
* Any (at least one) of the outer sets must match, within which all of the
|
||||
* individual filters must match.
|
||||
*/
|
||||
export type EntityFilters = Record<string, EntityFilter>;
|
||||
export type EntityFilter = {
|
||||
anyOf: { allOf: EntitiesSearchFilter[] }[];
|
||||
};
|
||||
|
||||
/**
|
||||
* An abstraction for transactions of the underlying database technology.
|
||||
@@ -160,10 +169,7 @@ export type Database = {
|
||||
matchingGeneration?: number,
|
||||
): Promise<DbEntityResponse>;
|
||||
|
||||
entities(
|
||||
tx: Transaction,
|
||||
filters?: EntityFilters[],
|
||||
): Promise<DbEntityResponse[]>;
|
||||
entities(tx: Transaction, filter?: EntityFilter): Promise<DbEntityResponse[]>;
|
||||
|
||||
entityByName(
|
||||
tx: Transaction,
|
||||
|
||||
@@ -18,7 +18,7 @@ import { Location, LocationSpec } from '@backstage/catalog-model';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { Logger } from 'winston';
|
||||
import { EntitiesCatalog, LocationsCatalog } from '../catalog';
|
||||
import { durationText } from '../util/timing';
|
||||
import { durationText } from '../util';
|
||||
import {
|
||||
AddLocationResult,
|
||||
HigherOrderOperation,
|
||||
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
|
||||
describe('CodeOwnersProcessor', () => {
|
||||
const mockUrl = ({ basePath = '' } = {}): string =>
|
||||
`https://github.com/spotify/backstage/blob/master/${basePath}catalog-info.yaml`;
|
||||
`https://github.com/backstage/backstage/blob/master/${basePath}catalog-info.yaml`;
|
||||
const mockLocation = ({
|
||||
basePath = '',
|
||||
type = 'github',
|
||||
@@ -39,12 +39,12 @@ describe('CodeOwnersProcessor', () => {
|
||||
});
|
||||
|
||||
const mockReadUrl = (basePath = '') =>
|
||||
`https://github.com/spotify/backstage/blob/master/${basePath}CODEOWNERS`;
|
||||
`https://github.com/backstage/backstage/blob/master/${basePath}CODEOWNERS`;
|
||||
|
||||
const mockGitUri = (codeOwnersPath: string = '') => {
|
||||
return {
|
||||
source: 'github.com',
|
||||
owner: 'spotify',
|
||||
owner: 'backstage',
|
||||
name: 'backstage',
|
||||
codeOwnersPath,
|
||||
};
|
||||
@@ -94,7 +94,7 @@ describe('CodeOwnersProcessor', () => {
|
||||
codeOwnersPath: '/.github/CODEOWNERS',
|
||||
}),
|
||||
).toBe(
|
||||
'https://github.com/spotify/backstage/blob/master/.github/CODEOWNERS',
|
||||
'https://github.com/backstage/backstage/blob/master/.github/CODEOWNERS',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -102,7 +102,7 @@ describe('CodeOwnersProcessor', () => {
|
||||
describe('buildCodeOwnerUrl', () => {
|
||||
it('should build a location spec to the codeowners', () => {
|
||||
expect(buildCodeOwnerUrl(mockUrl(), '/docs/CODEOWNERS')).toEqual(
|
||||
'https://github.com/spotify/backstage/blob/master/docs/CODEOWNERS',
|
||||
'https://github.com/backstage/backstage/blob/master/docs/CODEOWNERS',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -112,7 +112,9 @@ describe('CodeOwnersProcessor', () => {
|
||||
mockUrl({ basePath: 'packages/foo/' }),
|
||||
'/CODEOWNERS',
|
||||
),
|
||||
).toEqual('https://github.com/spotify/backstage/blob/master/CODEOWNERS');
|
||||
).toEqual(
|
||||
'https://github.com/backstage/backstage/blob/master/CODEOWNERS',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -182,7 +182,7 @@ export function readConfig(config: Config): ProviderConfig[] {
|
||||
export function parseUrl(urlString: string): { org: string } {
|
||||
const path = new URL(urlString).pathname.substr(1).split('/');
|
||||
|
||||
// /spotify
|
||||
// /backstage
|
||||
if (path.length === 1 && path[0].length) {
|
||||
return { org: decodeURIComponent(path[0]) };
|
||||
}
|
||||
|
||||
@@ -152,7 +152,8 @@ describe('PlaceholderProcessor', () => {
|
||||
},
|
||||
{
|
||||
type: 'github',
|
||||
target: 'https://github.com/spotify/backstage/a/b/catalog-info.yaml',
|
||||
target:
|
||||
'https://github.com/backstage/backstage/a/b/catalog-info.yaml',
|
||||
},
|
||||
),
|
||||
).resolves.toEqual({
|
||||
@@ -163,7 +164,7 @@ describe('PlaceholderProcessor', () => {
|
||||
});
|
||||
|
||||
expect(read).toBeCalledWith(
|
||||
'https://github.com/spotify/backstage/a/file.txt',
|
||||
'https://github.com/backstage/backstage/a/file.txt',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -186,7 +187,8 @@ describe('PlaceholderProcessor', () => {
|
||||
},
|
||||
{
|
||||
type: 'github',
|
||||
target: 'https://github.com/spotify/backstage/a/b/catalog-info.yaml',
|
||||
target:
|
||||
'https://github.com/backstage/backstage/a/b/catalog-info.yaml',
|
||||
},
|
||||
),
|
||||
).resolves.toEqual({
|
||||
@@ -197,7 +199,7 @@ describe('PlaceholderProcessor', () => {
|
||||
});
|
||||
|
||||
expect(read).toBeCalledWith(
|
||||
'https://github.com/spotify/backstage/a/b/file.json',
|
||||
'https://github.com/backstage/backstage/a/b/file.json',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -218,7 +220,8 @@ describe('PlaceholderProcessor', () => {
|
||||
},
|
||||
{
|
||||
type: 'github',
|
||||
target: 'https://github.com/spotify/backstage/a/b/catalog-info.yaml',
|
||||
target:
|
||||
'https://github.com/backstage/backstage/a/b/catalog-info.yaml',
|
||||
},
|
||||
),
|
||||
).resolves.toEqual({
|
||||
@@ -229,7 +232,7 @@ describe('PlaceholderProcessor', () => {
|
||||
});
|
||||
|
||||
expect(read).toBeCalledWith(
|
||||
'https://github.com/spotify/backstage/a/file.yaml',
|
||||
'https://github.com/backstage/backstage/a/file.yaml',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -248,13 +251,14 @@ describe('PlaceholderProcessor', () => {
|
||||
metadata: { name: 'n' },
|
||||
spec: {
|
||||
data: {
|
||||
$text: 'https://github.com/spotify/backstage/catalog-info.yaml',
|
||||
$text: 'https://github.com/backstage/backstage/catalog-info.yaml',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'github',
|
||||
target: 'https://github.com/spotify/backstage/a/b/catalog-info.yaml',
|
||||
target:
|
||||
'https://github.com/backstage/backstage/a/b/catalog-info.yaml',
|
||||
},
|
||||
),
|
||||
).resolves.toEqual({
|
||||
@@ -265,7 +269,7 @@ describe('PlaceholderProcessor', () => {
|
||||
});
|
||||
|
||||
expect(read).toBeCalledWith(
|
||||
'https://github.com/spotify/backstage/catalog-info.yaml',
|
||||
'https://github.com/backstage/backstage/catalog-info.yaml',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -284,7 +288,7 @@ describe('PlaceholderProcessor', () => {
|
||||
metadata: { name: 'n' },
|
||||
spec: {
|
||||
data: {
|
||||
$text: 'https://github.com/spotify/backstage/catalog-info.yaml',
|
||||
$text: 'https://github.com/backstage/backstage/catalog-info.yaml',
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -301,7 +305,7 @@ describe('PlaceholderProcessor', () => {
|
||||
});
|
||||
|
||||
expect(read).toBeCalledWith(
|
||||
'https://github.com/spotify/backstage/catalog-info.yaml',
|
||||
'https://github.com/backstage/backstage/catalog-info.yaml',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -345,7 +349,7 @@ describe('yamlPlaceholderResolver', () => {
|
||||
const params: ResolverParams = {
|
||||
key: 'a',
|
||||
value: './file.yaml',
|
||||
baseUrl: 'https://github.com/spotify/backstage/a/b/catalog-info.yaml',
|
||||
baseUrl: 'https://github.com/backstage/backstage/a/b/catalog-info.yaml',
|
||||
read,
|
||||
};
|
||||
|
||||
@@ -389,7 +393,7 @@ describe('jsonPlaceholderResolver', () => {
|
||||
const params: ResolverParams = {
|
||||
key: 'a',
|
||||
value: './file.json',
|
||||
baseUrl: 'https://github.com/spotify/backstage/a/b/catalog-info.yaml',
|
||||
baseUrl: 'https://github.com/backstage/backstage/a/b/catalog-info.yaml',
|
||||
read,
|
||||
};
|
||||
|
||||
|
||||
@@ -19,6 +19,8 @@ import * as results from './results';
|
||||
export { results };
|
||||
export * from './types';
|
||||
|
||||
export { parseEntityYaml } from './util/parse';
|
||||
|
||||
export { AnnotateLocationEntityProcessor } from './AnnotateLocationEntityProcessor';
|
||||
export { CodeOwnersProcessor } from './CodeOwnersProcessor';
|
||||
export { FileReaderProcessor } from './FileReaderProcessor';
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
import { Config, JsonValue } from '@backstage/config';
|
||||
import { SearchOptions } from 'ldapjs';
|
||||
import mergeWith from 'lodash/mergeWith';
|
||||
import { RecursivePartial } from './util';
|
||||
import { RecursivePartial } from '../../../util';
|
||||
|
||||
/**
|
||||
* The configuration parameters for a single LDAP provider.
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
import { GroupEntity, UserEntity } from '@backstage/catalog-model';
|
||||
import { SearchEntry } from 'ldapjs';
|
||||
import merge from 'lodash/merge';
|
||||
import { RecursivePartial } from '../../../util';
|
||||
import { LdapClient } from './client';
|
||||
import { GroupConfig, UserConfig } from './config';
|
||||
import {
|
||||
@@ -25,7 +26,6 @@ import {
|
||||
LDAP_UUID_ANNOTATION,
|
||||
} from './constants';
|
||||
import { readLdapGroups, readLdapUsers, resolveRelations } from './read';
|
||||
import { RecursivePartial } from './util';
|
||||
|
||||
function user(data: RecursivePartial<UserEntity>): UserEntity {
|
||||
return merge(
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { errorString, RecursivePartial } from './util';
|
||||
import { errorString } from './util';
|
||||
|
||||
describe('errorString', () => {
|
||||
it('formats', () => {
|
||||
@@ -22,17 +22,3 @@ describe('errorString', () => {
|
||||
expect(errorString(e)).toEqual('1 n: m');
|
||||
});
|
||||
});
|
||||
|
||||
describe('RecursivePartial', () => {
|
||||
it('is recursive', () => {
|
||||
type X = {
|
||||
required: {
|
||||
required: string;
|
||||
};
|
||||
};
|
||||
const x: RecursivePartial<X> = {
|
||||
required: {},
|
||||
};
|
||||
expect(x).toEqual({ required: {} });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,14 +24,3 @@ import { Error as LDAPError } from 'ldapjs';
|
||||
export function errorString(error: LDAPError) {
|
||||
return `${error.code} ${error.name}: ${error.message}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes all keys of an entire hierarchy optional.
|
||||
*/
|
||||
export type RecursivePartial<T> = {
|
||||
[P in keyof T]?: T[P] extends (infer U)[]
|
||||
? RecursivePartial<U>[]
|
||||
: T[P] extends object
|
||||
? RecursivePartial<T[P]>
|
||||
: T[P];
|
||||
};
|
||||
|
||||
@@ -34,7 +34,7 @@ describe('parseEntityYaml', () => {
|
||||
name: backstage
|
||||
description: backstage.io
|
||||
annotations:
|
||||
github.com/project-slug: 'spotify/backstage'
|
||||
github.com/project-slug: 'backstage/backstage'
|
||||
spec:
|
||||
type: website
|
||||
lifecycle: production
|
||||
@@ -54,7 +54,7 @@ describe('parseEntityYaml', () => {
|
||||
name: 'backstage',
|
||||
description: 'backstage.io',
|
||||
annotations: {
|
||||
'github.com/project-slug': 'spotify/backstage',
|
||||
'github.com/project-slug': 'backstage/backstage',
|
||||
},
|
||||
},
|
||||
spec: {
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* 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 { EntityFilters } from './EntityFilters';
|
||||
|
||||
describe('EntityFilters', () => {
|
||||
describe('ofQuery', () => {
|
||||
it('translates empty query to empty list', () => {
|
||||
const result = EntityFilters.ofQuery({});
|
||||
expect(result).toEqual(undefined);
|
||||
});
|
||||
|
||||
it('supports single-string format', () => {
|
||||
const result = EntityFilters.ofQuery({ filter: 'a=1' })!;
|
||||
expect(result).toEqual({
|
||||
anyOf: [{ allOf: [{ key: 'a', matchValueIn: ['1'] }] }],
|
||||
});
|
||||
});
|
||||
|
||||
it('supports array-of-strings format', () => {
|
||||
const result = EntityFilters.ofQuery({ filter: ['a=1', 'b=2'] });
|
||||
expect(result).toEqual({
|
||||
anyOf: [
|
||||
{ allOf: [{ key: 'a', matchValueIn: ['1'] }] },
|
||||
{ allOf: [{ key: 'b', matchValueIn: ['2'] }] },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('throws for non-strings', () => {
|
||||
expect(() => EntityFilters.ofQuery({ filter: [3] })).toThrow(/string/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ofFilterString', () => {
|
||||
it('runs the happy path', () => {
|
||||
const result = EntityFilters.ofFilterString('a=1,b=2');
|
||||
expect(result).toEqual({
|
||||
anyOf: [
|
||||
{
|
||||
allOf: [
|
||||
{ key: 'a', matchValueIn: ['1'] },
|
||||
{ key: 'b', matchValueIn: ['2'] },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('ignores empty', () => {
|
||||
const result = EntityFilters.ofFilterString('a=1,,b=2,');
|
||||
expect(result).toEqual({
|
||||
anyOf: [
|
||||
{
|
||||
allOf: [
|
||||
{ key: 'a', matchValueIn: ['1'] },
|
||||
{ key: 'b', matchValueIn: ['2'] },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('trims', () => {
|
||||
const result = EntityFilters.ofFilterString(' a = 1 ,, b=2 ,');
|
||||
expect(result).toEqual({
|
||||
anyOf: [
|
||||
{
|
||||
allOf: [
|
||||
{ key: 'a', matchValueIn: ['1'] },
|
||||
{ key: 'b', matchValueIn: ['2'] },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('merges multiple of the same key', () => {
|
||||
const result = EntityFilters.ofFilterString('a=1,a=2,b=3');
|
||||
expect(result).toEqual({
|
||||
anyOf: [
|
||||
{
|
||||
allOf: [
|
||||
{ key: 'a', matchValueIn: ['1', '2'] },
|
||||
{ key: 'b', matchValueIn: ['3'] },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('throws on missing equal sign', () => {
|
||||
expect(() => EntityFilters.ofFilterString('a,b=2')).toThrow();
|
||||
});
|
||||
|
||||
it('throws on misplaced equal sign', () => {
|
||||
expect(() => EntityFilters.ofFilterString('a=,b=2')).toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* 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 { InputError } from '@backstage/backend-common';
|
||||
import { EntitiesSearchFilter, EntityFilter } from '../database';
|
||||
|
||||
/**
|
||||
* A builder that assists in creating entity filter instances.
|
||||
*/
|
||||
export class EntityFilters {
|
||||
// Builds a filter from the value of a filter=a=1,b=2 type filter string
|
||||
static ofFilterString(filterString: string): EntityFilter | undefined {
|
||||
const builder = new EntityFilters();
|
||||
builder.addFilterString(filterString);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
// Builds a filter from an entire query params structure
|
||||
static ofQuery(query: Record<string, any>): EntityFilter | undefined {
|
||||
const filterStrings = [query.filter || []].flat();
|
||||
|
||||
if (filterStrings.some(f => typeof f !== 'string')) {
|
||||
throw new InputError(
|
||||
'Only string type filter query parameters are supported',
|
||||
);
|
||||
}
|
||||
|
||||
const builder = new EntityFilters();
|
||||
for (const filterString of filterStrings) {
|
||||
builder.addFilterString(filterString);
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
// Builds a filter from a { key: value } matcher object
|
||||
static ofMatchers(
|
||||
matchers: Record<string, string | string[]>,
|
||||
): EntityFilter | undefined {
|
||||
const builder = new EntityFilters();
|
||||
builder.addMatchers(matchers);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private filters: EntitiesSearchFilter[][];
|
||||
|
||||
constructor() {
|
||||
this.filters = [];
|
||||
}
|
||||
|
||||
addFilterString(filterString: string): EntityFilters {
|
||||
const filtersByKey: Record<string, EntitiesSearchFilter> = {};
|
||||
|
||||
const addFilter = (key: string, value: string) => {
|
||||
const f =
|
||||
key in filtersByKey
|
||||
? filtersByKey[key]
|
||||
: (filtersByKey[key] = { key, matchValueIn: [] });
|
||||
f.matchValueIn!.push(value);
|
||||
};
|
||||
|
||||
const statements = filterString
|
||||
.split(',')
|
||||
.map(s => s.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
for (const statement of statements) {
|
||||
const equalsIndex = statement.indexOf('=');
|
||||
if (equalsIndex < 1) {
|
||||
throw new InputError('Malformed filter query');
|
||||
} else {
|
||||
const key = statement.substr(0, equalsIndex).trim();
|
||||
const value = statement.substr(equalsIndex + 1).trim();
|
||||
if (!key || !value) {
|
||||
throw new InputError('Malformed filter query');
|
||||
}
|
||||
addFilter(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
this.filters.push(Object.values(filtersByKey));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
addMatchers(matchers: Record<string, string | string[]>): EntityFilters {
|
||||
const filters: EntitiesSearchFilter[] = [];
|
||||
|
||||
for (const [key, value] of Object.entries(matchers)) {
|
||||
filters.push({ key, matchValueIn: [value].flat() });
|
||||
}
|
||||
|
||||
if (filters.length) {
|
||||
this.filters.push(filters);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
build(): EntityFilter | undefined {
|
||||
if (!this.filters.length) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return { anyOf: this.filters.map(f => ({ allOf: f })) };
|
||||
}
|
||||
}
|
||||
@@ -15,66 +15,7 @@
|
||||
*/
|
||||
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import {
|
||||
translateFilterQueryEntryToEntityFilters,
|
||||
translateQueryToEntityFilters,
|
||||
translateQueryToFieldMapper,
|
||||
} from './filterQuery';
|
||||
|
||||
describe('translateQueryToEntityFilters', () => {
|
||||
it('translates empty query to empty list', () => {
|
||||
const result = translateQueryToEntityFilters({});
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('supports single-string format', () => {
|
||||
const result = translateQueryToEntityFilters({ filter: 'a=1' });
|
||||
expect(result).toEqual([{ a: ['1'] }]);
|
||||
});
|
||||
|
||||
it('supports array-of-strings format', () => {
|
||||
const result = translateQueryToEntityFilters({ filter: ['a=1', 'b=2'] });
|
||||
expect(result).toEqual([{ a: ['1'] }, { b: ['2'] }]);
|
||||
});
|
||||
|
||||
it('throws for non-strings', () => {
|
||||
expect(() => translateQueryToEntityFilters({ filter: [3] })).toThrow(
|
||||
/string/,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('translateFilterQueryEntryToEntityFilters', () => {
|
||||
it('runs the happy path', () => {
|
||||
const result = translateFilterQueryEntryToEntityFilters('a=1,b=2');
|
||||
expect(result).toEqual({ a: ['1'], b: ['2'] });
|
||||
});
|
||||
|
||||
it('ignores empty', () => {
|
||||
const result = translateFilterQueryEntryToEntityFilters('a=1,,b=2,');
|
||||
expect(result).toEqual({ a: ['1'], b: ['2'] });
|
||||
});
|
||||
|
||||
it('trims', () => {
|
||||
const result = translateFilterQueryEntryToEntityFilters(' a = 1 ,, b=2 ,');
|
||||
expect(result).toEqual({ a: ['1'], b: ['2'] });
|
||||
});
|
||||
|
||||
it('merges multiple of the same key', () => {
|
||||
const result = translateFilterQueryEntryToEntityFilters('a=1,a=2,b=3');
|
||||
expect(result).toEqual({ a: ['1', '2'], b: ['3'] });
|
||||
});
|
||||
|
||||
it('treats missing equal signs as presence', () => {
|
||||
const result = translateFilterQueryEntryToEntityFilters('a,b=2');
|
||||
expect(result).toEqual({ a: ['*'], b: ['2'] });
|
||||
});
|
||||
|
||||
it('treats empty value as null/absence', () => {
|
||||
const result = translateFilterQueryEntryToEntityFilters('a=,b=2');
|
||||
expect(result).toEqual({ a: [null], b: ['2'] });
|
||||
});
|
||||
});
|
||||
import { translateQueryToFieldMapper } from './filterQuery';
|
||||
|
||||
describe('translateQueryToFieldMapper', () => {
|
||||
const entity: Entity = {
|
||||
|
||||
@@ -17,62 +17,7 @@
|
||||
import { InputError } from '@backstage/backend-common';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import lodash from 'lodash';
|
||||
import { EntityFilters } from '../database';
|
||||
import { RecursivePartial } from '../ingestion/processors/ldap/util';
|
||||
|
||||
export function translateQueryToEntityFilters(
|
||||
query: Record<string, any>,
|
||||
): EntityFilters[] {
|
||||
if (!query.filter) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const filterStrings = [query.filter].flat();
|
||||
|
||||
if (filterStrings.some(s => typeof s !== 'string')) {
|
||||
throw new InputError(
|
||||
'Only string type filter query parameters are supported',
|
||||
);
|
||||
}
|
||||
|
||||
return filterStrings
|
||||
.filter(Boolean)
|
||||
.map(translateFilterQueryEntryToEntityFilters);
|
||||
}
|
||||
|
||||
// Parses the value of a filter=a=1,b=2 type query param
|
||||
export function translateFilterQueryEntryToEntityFilters(
|
||||
filterString: string,
|
||||
): EntityFilters {
|
||||
const filters: Record<string, (string | null)[]> = {};
|
||||
|
||||
const addFilter = (key: string, value: string | null) => {
|
||||
const matchers = key in filters ? filters[key] : (filters[key] = []);
|
||||
matchers.push(value || null);
|
||||
};
|
||||
|
||||
const statements = filterString
|
||||
.split(',')
|
||||
.map(s => s.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
for (const statement of statements) {
|
||||
const equalsIndex = statement.indexOf('=');
|
||||
if (equalsIndex < 0) {
|
||||
// Check presence, any value
|
||||
addFilter(statement, '*');
|
||||
} else {
|
||||
const key = statement.substr(0, equalsIndex).trim();
|
||||
const value = statement.substr(equalsIndex + 1).trim();
|
||||
if (!key) {
|
||||
throw new InputError('Malformed filter query');
|
||||
}
|
||||
addFilter(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
return filters;
|
||||
}
|
||||
import { RecursivePartial } from '../util';
|
||||
|
||||
type FieldMapper = (entity: Entity) => Entity;
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import request from 'supertest';
|
||||
import { EntitiesCatalog, LocationsCatalog } from '../catalog';
|
||||
import { LocationResponse } from '../catalog/types';
|
||||
import { HigherOrderOperation } from '../ingestion/types';
|
||||
import { EntityFilters } from './EntityFilters';
|
||||
import { createRouter } from './router';
|
||||
|
||||
describe('createRouter', () => {
|
||||
@@ -78,18 +79,22 @@ describe('createRouter', () => {
|
||||
it('parses single and multiple request parameters and passes them down', async () => {
|
||||
entitiesCatalog.entities.mockResolvedValueOnce([]);
|
||||
const response = await request(app).get(
|
||||
'/entities?filter=a=1,a=,a=3,b=4&filter=c=',
|
||||
'/entities?filter=a=1,a=2,b=3&filter=c=4',
|
||||
);
|
||||
|
||||
expect(response.status).toEqual(200);
|
||||
expect(entitiesCatalog.entities).toHaveBeenCalledTimes(1);
|
||||
expect(entitiesCatalog.entities).toHaveBeenCalledWith([
|
||||
{
|
||||
a: ['1', null, '3'],
|
||||
b: ['4'],
|
||||
},
|
||||
{ c: [null] },
|
||||
]);
|
||||
expect(entitiesCatalog.entities).toHaveBeenCalledWith({
|
||||
anyOf: [
|
||||
{
|
||||
allOf: [
|
||||
{ key: 'a', matchValueIn: ['1', '2'] },
|
||||
{ key: 'b', matchValueIn: ['3'] },
|
||||
],
|
||||
},
|
||||
{ allOf: [{ key: 'c', matchValueIn: ['4'] }] },
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -107,9 +112,9 @@ describe('createRouter', () => {
|
||||
const response = await request(app).get('/entities/by-uid/zzz');
|
||||
|
||||
expect(entitiesCatalog.entities).toHaveBeenCalledTimes(1);
|
||||
expect(entitiesCatalog.entities).toHaveBeenCalledWith([
|
||||
{ 'metadata.uid': 'zzz' },
|
||||
]);
|
||||
expect(entitiesCatalog.entities).toHaveBeenCalledWith(
|
||||
EntityFilters.ofMatchers({ 'metadata.uid': 'zzz' }),
|
||||
);
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual(expect.objectContaining(entity));
|
||||
});
|
||||
@@ -120,9 +125,9 @@ describe('createRouter', () => {
|
||||
const response = await request(app).get('/entities/by-uid/zzz');
|
||||
|
||||
expect(entitiesCatalog.entities).toHaveBeenCalledTimes(1);
|
||||
expect(entitiesCatalog.entities).toHaveBeenCalledWith([
|
||||
{ 'metadata.uid': 'zzz' },
|
||||
]);
|
||||
expect(entitiesCatalog.entities).toHaveBeenCalledWith(
|
||||
EntityFilters.ofMatchers({ 'metadata.uid': 'zzz' }),
|
||||
);
|
||||
expect(response.status).toEqual(404);
|
||||
expect(response.text).toMatch(/uid/);
|
||||
});
|
||||
@@ -143,13 +148,13 @@ describe('createRouter', () => {
|
||||
const response = await request(app).get('/entities/by-name/k/ns/n');
|
||||
|
||||
expect(entitiesCatalog.entities).toHaveBeenCalledTimes(1);
|
||||
expect(entitiesCatalog.entities).toHaveBeenCalledWith([
|
||||
{
|
||||
expect(entitiesCatalog.entities).toHaveBeenCalledWith(
|
||||
EntityFilters.ofMatchers({
|
||||
kind: 'k',
|
||||
'metadata.namespace': 'ns',
|
||||
'metadata.name': 'n',
|
||||
},
|
||||
]);
|
||||
}),
|
||||
);
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual(expect.objectContaining(entity));
|
||||
});
|
||||
@@ -160,13 +165,13 @@ describe('createRouter', () => {
|
||||
const response = await request(app).get('/entities/by-name/b/d/c');
|
||||
|
||||
expect(entitiesCatalog.entities).toHaveBeenCalledTimes(1);
|
||||
expect(entitiesCatalog.entities).toHaveBeenCalledWith([
|
||||
{
|
||||
expect(entitiesCatalog.entities).toHaveBeenCalledWith(
|
||||
EntityFilters.ofMatchers({
|
||||
kind: 'b',
|
||||
'metadata.namespace': 'd',
|
||||
'metadata.name': 'c',
|
||||
},
|
||||
]);
|
||||
}),
|
||||
);
|
||||
expect(response.status).toEqual(404);
|
||||
expect(response.text).toMatch(/name/);
|
||||
});
|
||||
@@ -209,9 +214,9 @@ describe('createRouter', () => {
|
||||
{ entity, relations: [] },
|
||||
]);
|
||||
expect(entitiesCatalog.entities).toHaveBeenCalledTimes(1);
|
||||
expect(entitiesCatalog.entities).toHaveBeenCalledWith([
|
||||
{ 'metadata.uid': 'u' },
|
||||
]);
|
||||
expect(entitiesCatalog.entities).toHaveBeenCalledWith(
|
||||
EntityFilters.ofMatchers({ 'metadata.uid': 'u' }),
|
||||
);
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual(entity);
|
||||
});
|
||||
|
||||
@@ -23,10 +23,8 @@ import { Logger } from 'winston';
|
||||
import yn from 'yn';
|
||||
import { EntitiesCatalog, LocationsCatalog } from '../catalog';
|
||||
import { HigherOrderOperation } from '../ingestion/types';
|
||||
import {
|
||||
translateQueryToEntityFilters,
|
||||
translateQueryToFieldMapper,
|
||||
} from './filterQuery';
|
||||
import { EntityFilters } from './EntityFilters';
|
||||
import { translateQueryToFieldMapper } from './filterQuery';
|
||||
import { requireRequestBody, validateRequestBody } from './util';
|
||||
|
||||
export interface RouterOptions {
|
||||
@@ -47,9 +45,9 @@ export async function createRouter(
|
||||
if (entitiesCatalog) {
|
||||
router
|
||||
.get('/entities', async (req, res) => {
|
||||
const filters = translateQueryToEntityFilters(req.query);
|
||||
const filter = EntityFilters.ofQuery(req.query);
|
||||
const fieldMapper = translateQueryToFieldMapper(req.query);
|
||||
const entities = await entitiesCatalog.entities(filters);
|
||||
const entities = await entitiesCatalog.entities(filter);
|
||||
res.status(200).send(entities.map(fieldMapper));
|
||||
})
|
||||
.post('/entities', async (req, res) => {
|
||||
@@ -57,18 +55,16 @@ export async function createRouter(
|
||||
const [result] = await entitiesCatalog.batchAddOrUpdateEntities([
|
||||
{ entity: body as Entity, relations: [] },
|
||||
]);
|
||||
const [entity] = await entitiesCatalog.entities([
|
||||
{ 'metadata.uid': result.entityId },
|
||||
]);
|
||||
const [entity] = await entitiesCatalog.entities(
|
||||
EntityFilters.ofMatchers({ 'metadata.uid': result.entityId }),
|
||||
);
|
||||
res.status(200).send(entity);
|
||||
})
|
||||
.get('/entities/by-uid/:uid', async (req, res) => {
|
||||
const { uid } = req.params;
|
||||
const entities = await entitiesCatalog.entities([
|
||||
{
|
||||
'metadata.uid': uid,
|
||||
},
|
||||
]);
|
||||
const entities = await entitiesCatalog.entities(
|
||||
EntityFilters.ofMatchers({ 'metadata.uid': uid }),
|
||||
);
|
||||
if (!entities.length) {
|
||||
res.status(404).send(`No entity with uid ${uid}`);
|
||||
}
|
||||
@@ -81,13 +77,13 @@ export async function createRouter(
|
||||
})
|
||||
.get('/entities/by-name/:kind/:namespace/:name', async (req, res) => {
|
||||
const { kind, namespace, name } = req.params;
|
||||
const entities = await entitiesCatalog.entities([
|
||||
{
|
||||
const entities = await entitiesCatalog.entities(
|
||||
EntityFilters.ofMatchers({
|
||||
kind: kind,
|
||||
'metadata.namespace': namespace,
|
||||
'metadata.name': name,
|
||||
},
|
||||
]);
|
||||
}),
|
||||
);
|
||||
if (!entities.length) {
|
||||
res
|
||||
.status(404)
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* 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 { RecursivePartial } from './RecursivePartial';
|
||||
|
||||
describe('RecursivePartial', () => {
|
||||
it('is recursive', () => {
|
||||
type X = {
|
||||
required: {
|
||||
required: string;
|
||||
};
|
||||
};
|
||||
const x: RecursivePartial<X> = {
|
||||
required: {},
|
||||
};
|
||||
expect(x).toEqual({ required: {} });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Makes all keys of an entire hierarchy optional.
|
||||
*/
|
||||
export type RecursivePartial<T> = {
|
||||
[P in keyof T]?: T[P] extends (infer U)[]
|
||||
? RecursivePartial<U>[]
|
||||
: T[P] extends object
|
||||
? RecursivePartial<T[P]>
|
||||
: T[P];
|
||||
};
|
||||
@@ -15,3 +15,5 @@
|
||||
*/
|
||||
|
||||
export * from './runPeriodically';
|
||||
export * from './RecursivePartial';
|
||||
export * from './timing';
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
# @backstage/plugin-catalog-graphql
|
||||
|
||||
## 0.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 28edd7d29: Create backend plugin through CLI
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [3a4236570]
|
||||
- Updated dependencies [e0be86b6f]
|
||||
- Updated dependencies [f70a52868]
|
||||
- Updated dependencies [12b5fe940]
|
||||
- Updated dependencies [5249594c5]
|
||||
- Updated dependencies [56e4eb589]
|
||||
- Updated dependencies [e37c0a005]
|
||||
- Updated dependencies [a768a07fb]
|
||||
- Updated dependencies [f00ca3cb8]
|
||||
- Updated dependencies [6579769df]
|
||||
- Updated dependencies [5adfc005e]
|
||||
- Updated dependencies [8c2b76e45]
|
||||
- Updated dependencies [440a17b39]
|
||||
- Updated dependencies [fa56f4615]
|
||||
- Updated dependencies [8afce088a]
|
||||
- Updated dependencies [b3d57961c]
|
||||
- Updated dependencies [7bbeb049f]
|
||||
- @backstage/catalog-model@0.2.0
|
||||
- @backstage/backend-common@0.2.0
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-catalog-graphql",
|
||||
"version": "0.1.1-alpha.26",
|
||||
"version": "0.2.0",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -20,9 +20,9 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-common": "^0.1.1-alpha.26",
|
||||
"@backstage/catalog-model": "^0.1.1-alpha.26",
|
||||
"@backstage/config": "^0.1.1-alpha.26",
|
||||
"@backstage/backend-common": "^0.2.0",
|
||||
"@backstage/catalog-model": "^0.2.0",
|
||||
"@backstage/config": "^0.1.1",
|
||||
"@graphql-modules/core": "^0.7.17",
|
||||
"apollo-server": "^2.16.1",
|
||||
"cross-fetch": "^3.0.6",
|
||||
@@ -32,8 +32,8 @@
|
||||
"winston": "^3.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.26",
|
||||
"@backstage/test-utils": "^0.1.1-alpha.26",
|
||||
"@backstage/cli": "^0.2.0",
|
||||
"@backstage/test-utils": "^0.1.2",
|
||||
"@graphql-codegen/cli": "^1.17.7",
|
||||
"@graphql-codegen/typescript": "^1.17.7",
|
||||
"@graphql-codegen/typescript-resolvers": "^1.17.7",
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
# @backstage/plugin-catalog
|
||||
|
||||
## 0.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 28edd7d29: Create backend plugin through CLI
|
||||
- 368fd8243: Created EntityNotFound component for catalog which displays the 404 page when entity is not found.
|
||||
|
||||
Fixes #2266
|
||||
|
||||
- 6d97d2d6f: The InfoCard variant `'height100'` is deprecated. Use variant `'gridItem'` instead.
|
||||
|
||||
When the InfoCard is displayed as a grid item within a grid, you may want items to have the same height for all items.
|
||||
Set to the `'gridItem'` variant to display the InfoCard with full height suitable for Grid:
|
||||
`<InfoCard variant="gridItem">...</InfoCard>`
|
||||
|
||||
Changed the InfoCards in '@backstage/plugin-github-actions', '@backstage/plugin-jenkins', '@backstage/plugin-lighthouse'
|
||||
to pass an optional variant to the corresponding card of the plugin.
|
||||
|
||||
As a result the overview content of the EntityPage shows cards with full height suitable for Grid.
|
||||
|
||||
- f0aa01bcc: Add client side paging for catalog table
|
||||
- 8b9c8196f: Locations registered through the catalog client now default to the 'url' type. The type selection dropdown in the register-component form has been removed.
|
||||
- 2ebcfac8d: Add a validate button to the register-component page
|
||||
|
||||
This allows the user to validate his location before adding it.
|
||||
|
||||
- 0b956f21b: The URL path for a catalog entity has changed,
|
||||
|
||||
- from: `/catalog/:kind/:optionalNamespaceAndName`
|
||||
- to: `/catalog/:namespace/:kind/:name`
|
||||
|
||||
Redirects are in place, so disruptions for users should not happen.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 0aecfded0: handle the case where no entities are available to show
|
||||
- 60d40892c: Remove "in default" in component name
|
||||
- 97c2cb19b: update the EntityNotFound component
|
||||
- Updated dependencies [28edd7d29]
|
||||
- Updated dependencies [819a70229]
|
||||
- Updated dependencies [3a4236570]
|
||||
- Updated dependencies [ae5983387]
|
||||
- Updated dependencies [0d4459c08]
|
||||
- Updated dependencies [482b6313d]
|
||||
- Updated dependencies [e0be86b6f]
|
||||
- Updated dependencies [f70a52868]
|
||||
- Updated dependencies [12b5fe940]
|
||||
- Updated dependencies [8351ad79b]
|
||||
- Updated dependencies [fb74f1db6]
|
||||
- Updated dependencies [1c60f716e]
|
||||
- Updated dependencies [144c66d50]
|
||||
- Updated dependencies [a768a07fb]
|
||||
- Updated dependencies [b79017fd3]
|
||||
- Updated dependencies [6d97d2d6f]
|
||||
- Updated dependencies [5adfc005e]
|
||||
- Updated dependencies [93a3fa3ae]
|
||||
- Updated dependencies [782f3b354]
|
||||
- Updated dependencies [c5ef12926]
|
||||
- Updated dependencies [2713f28f4]
|
||||
- Updated dependencies [406015b0d]
|
||||
- Updated dependencies [82759d3e4]
|
||||
- Updated dependencies [ac8d5d5c7]
|
||||
- Updated dependencies [fa56f4615]
|
||||
- Updated dependencies [ebca83d48]
|
||||
- Updated dependencies [aca79334f]
|
||||
- Updated dependencies [c0d5242a0]
|
||||
- Updated dependencies [b3d57961c]
|
||||
- Updated dependencies [1c8c43756]
|
||||
- Updated dependencies [3beb5c9fc]
|
||||
- Updated dependencies [754e31db5]
|
||||
- Updated dependencies [57b54c8ed]
|
||||
- Updated dependencies [1611c6dbc]
|
||||
- @backstage/plugin-scaffolder@0.2.0
|
||||
- @backstage/plugin-techdocs@0.2.0
|
||||
- @backstage/core@0.2.0
|
||||
- @backstage/catalog-model@0.2.0
|
||||
- @backstage/theme@0.2.0
|
||||
@@ -9,5 +9,5 @@ supply the base views to show and manage them.
|
||||
|
||||
## Links
|
||||
|
||||
- [Backend part of the plugin](https://github.com/spotify/backstage/tree/master/plugins/catalog-backend)
|
||||
- [Backend part of the plugin](https://github.com/backstage/backstage/tree/master/plugins/catalog-backend)
|
||||
- [The Backstage homepage](https://backstage.io)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-catalog",
|
||||
"version": "0.1.1-alpha.26",
|
||||
"version": "0.2.0",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -21,11 +21,12 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/catalog-model": "^0.1.1-alpha.26",
|
||||
"@backstage/core": "^0.1.1-alpha.26",
|
||||
"@backstage/plugin-scaffolder": "^0.1.1-alpha.26",
|
||||
"@backstage/plugin-techdocs": "^0.1.1-alpha.26",
|
||||
"@backstage/theme": "^0.1.1-alpha.26",
|
||||
"@backstage/catalog-client": "^0.2.0",
|
||||
"@backstage/catalog-model": "^0.2.0",
|
||||
"@backstage/core": "^0.2.0",
|
||||
"@backstage/plugin-scaffolder": "^0.2.0",
|
||||
"@backstage/plugin-techdocs": "^0.2.0",
|
||||
"@backstage/theme": "^0.2.0",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
@@ -41,9 +42,9 @@
|
||||
"swr": "^0.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.26",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.26",
|
||||
"@backstage/test-utils": "^0.1.1-alpha.26",
|
||||
"@backstage/cli": "^0.2.0",
|
||||
"@backstage/dev-utils": "^0.1.2",
|
||||
"@backstage/test-utils": "^0.1.2",
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^10.4.1",
|
||||
"@testing-library/react-hooks": "^3.3.0",
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
/*
|
||||
* 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 { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { CatalogClient } from './CatalogClient';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { UrlPatternDiscovery } from '@backstage/core';
|
||||
import { msw } from '@backstage/test-utils';
|
||||
|
||||
const server = setupServer();
|
||||
const mockBaseUrl = 'http://backstage:9191/i-am-a-mock-base';
|
||||
const discoveryApi = UrlPatternDiscovery.compile(mockBaseUrl);
|
||||
|
||||
describe('CatalogClient', () => {
|
||||
let client = new CatalogClient({ discoveryApi });
|
||||
|
||||
msw.setupDefaultHandlers(server);
|
||||
|
||||
beforeEach(() => {
|
||||
client = new CatalogClient({ discoveryApi });
|
||||
});
|
||||
|
||||
describe('getEntities', () => {
|
||||
const defaultResponse: Entity[] = [
|
||||
{
|
||||
apiVersion: '1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'Test1',
|
||||
namespace: 'test1',
|
||||
},
|
||||
},
|
||||
{
|
||||
apiVersion: '1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'Test2',
|
||||
namespace: 'test1',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
server.use(
|
||||
rest.get(`${mockBaseUrl}/entities`, (_, res, ctx) => {
|
||||
return res(ctx.json(defaultResponse));
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should entities from correct endpoint', async () => {
|
||||
const entities = await client.getEntities();
|
||||
expect(entities).toEqual(defaultResponse);
|
||||
});
|
||||
|
||||
it('builds entity search filters properly', async () => {
|
||||
expect.assertions(2);
|
||||
server.use(
|
||||
rest.get(`${mockBaseUrl}/entities`, (req, res, ctx) => {
|
||||
expect(req.url.search).toBe('?filter=a=1,b=2,b=3,%C3%B6=%3D');
|
||||
return res(ctx.json([]));
|
||||
}),
|
||||
);
|
||||
|
||||
const entities = await client.getEntities({
|
||||
a: '1',
|
||||
b: ['2', '3'],
|
||||
ö: '=',
|
||||
});
|
||||
|
||||
expect(entities).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,157 +0,0 @@
|
||||
/*
|
||||
* 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,
|
||||
Location,
|
||||
LOCATION_ANNOTATION,
|
||||
} from '@backstage/catalog-model';
|
||||
import {
|
||||
AddLocationRequest,
|
||||
AddLocationResponse,
|
||||
CatalogApi,
|
||||
EntityCompoundName,
|
||||
} from './types';
|
||||
import { DiscoveryApi } from '@backstage/core';
|
||||
|
||||
export class CatalogClient implements CatalogApi {
|
||||
private readonly discoveryApi: DiscoveryApi;
|
||||
|
||||
constructor(options: { discoveryApi: DiscoveryApi }) {
|
||||
this.discoveryApi = options.discoveryApi;
|
||||
}
|
||||
|
||||
private async getRequired(path: string): Promise<any> {
|
||||
const url = `${await this.discoveryApi.getBaseUrl('catalog')}${path}`;
|
||||
const response = await fetch(url);
|
||||
|
||||
if (!response.ok) {
|
||||
const payload = await response.text();
|
||||
const message = `Request failed with ${response.status} ${response.statusText}, ${payload}`;
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
private async getOptional(path: string): Promise<any | undefined> {
|
||||
const url = `${await this.discoveryApi.getBaseUrl('catalog')}${path}`;
|
||||
const response = await fetch(url);
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const payload = await response.text();
|
||||
const message = `Request failed with ${response.status} ${response.statusText}, ${payload}`;
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
async getLocationById(id: String): Promise<Location | undefined> {
|
||||
return await this.getOptional(`/locations/${id}`);
|
||||
}
|
||||
|
||||
async getEntities(
|
||||
filter?: Record<string, string | string[]>,
|
||||
): Promise<Entity[]> {
|
||||
let path = `/entities`;
|
||||
if (filter) {
|
||||
const parts: string[] = [];
|
||||
for (const [key, value] of Object.entries(filter)) {
|
||||
for (const v of [value].flat()) {
|
||||
parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(v)}`);
|
||||
}
|
||||
}
|
||||
path += `?filter=${parts.join(',')}`;
|
||||
}
|
||||
|
||||
return await this.getRequired(path);
|
||||
}
|
||||
|
||||
async getEntityByName(
|
||||
compoundName: EntityCompoundName,
|
||||
): Promise<Entity | undefined> {
|
||||
const { kind, namespace = 'default', name } = compoundName;
|
||||
return this.getOptional(`/entities/by-name/${kind}/${namespace}/${name}`);
|
||||
}
|
||||
|
||||
async addLocation({
|
||||
type = 'url',
|
||||
target,
|
||||
dryRun,
|
||||
}: AddLocationRequest): Promise<AddLocationResponse> {
|
||||
const response = await fetch(
|
||||
`${await this.discoveryApi.getBaseUrl('catalog')}/locations${
|
||||
dryRun ? '?dryRun=true' : ''
|
||||
}`,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ type, target }),
|
||||
},
|
||||
);
|
||||
|
||||
if (response.status !== 201) {
|
||||
throw new Error(await response.text());
|
||||
}
|
||||
|
||||
const { location, entities } = await response.json();
|
||||
|
||||
if (!location) {
|
||||
throw new Error(`Location wasn't added: ${target}`);
|
||||
}
|
||||
|
||||
if (entities.length === 0) {
|
||||
throw new Error(
|
||||
`Location was added but has no entities specified yet: ${target}`,
|
||||
);
|
||||
}
|
||||
return {
|
||||
location,
|
||||
entities,
|
||||
};
|
||||
}
|
||||
|
||||
async getLocationByEntity(entity: Entity): Promise<Location | undefined> {
|
||||
const locationCompound = entity.metadata.annotations?.[LOCATION_ANNOTATION];
|
||||
const all: { data: Location }[] = await this.getRequired('/locations');
|
||||
return all
|
||||
.map(r => r.data)
|
||||
.find(l => locationCompound === `${l.type}:${l.target}`);
|
||||
}
|
||||
|
||||
async removeEntityByUid(uid: string): Promise<void> {
|
||||
const response = await fetch(
|
||||
`${await this.discoveryApi.getBaseUrl('catalog')}/entities/by-uid/${uid}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
},
|
||||
);
|
||||
if (!response.ok) {
|
||||
const payload = await response.text();
|
||||
throw new Error(
|
||||
`Request failed with ${response.status} ${response.statusText}, ${payload}`,
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
/*
|
||||
* 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 { createApiRef } from '@backstage/core';
|
||||
import { Entity, Location } from '@backstage/catalog-model';
|
||||
|
||||
export const catalogApiRef = createApiRef<CatalogApi>({
|
||||
id: 'plugin.catalog.service',
|
||||
description:
|
||||
'Used by the Catalog plugin to make requests to accompanying backend',
|
||||
});
|
||||
|
||||
export type EntityCompoundName = {
|
||||
kind: string;
|
||||
namespace?: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export interface CatalogApi {
|
||||
getLocationById(id: String): Promise<Location | undefined>;
|
||||
getEntityByName(
|
||||
compoundName: EntityCompoundName,
|
||||
): Promise<Entity | undefined>;
|
||||
getEntities(filter?: Record<string, string | string[]>): Promise<Entity[]>;
|
||||
addLocation(location: AddLocationRequest): Promise<AddLocationResponse>;
|
||||
getLocationByEntity(entity: Entity): Promise<Location | undefined>;
|
||||
removeEntityByUid(uid: string): Promise<void>;
|
||||
}
|
||||
|
||||
export type AddLocationRequest = {
|
||||
type?: string;
|
||||
target: string;
|
||||
dryRun?: boolean;
|
||||
};
|
||||
|
||||
export type AddLocationResponse = {
|
||||
location: Location;
|
||||
entities: Entity[];
|
||||
};
|
||||
@@ -27,7 +27,7 @@ describe('<AboutCard />', () => {
|
||||
name: 'software',
|
||||
annotations: {
|
||||
'backstage.io/managed-by-location':
|
||||
'github:https://github.com/spotify/backstage/blob/master/software.yaml',
|
||||
'github:https://github.com/backstage/backstage/blob/master/software.yaml',
|
||||
},
|
||||
},
|
||||
spec: {
|
||||
@@ -40,7 +40,7 @@ describe('<AboutCard />', () => {
|
||||
expect(getByText('service')).toBeInTheDocument();
|
||||
expect(getByText('View Source').closest('a')).toHaveAttribute(
|
||||
'href',
|
||||
'https://github.com/spotify/backstage/blob/master/software.yaml',
|
||||
'https://github.com/backstage/backstage/blob/master/software.yaml',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -87,7 +87,7 @@ function getCodeLinkInfo(entity: Entity): CodeLinkInfo {
|
||||
|
||||
if (location) {
|
||||
// split by first `:`
|
||||
// e.g. "github:https://github.com/spotify/backstage/blob/master/software.yaml"
|
||||
// e.g. "github:https://github.com/backstage/backstage/blob/master/software.yaml"
|
||||
const [type, target] = location.split(/:(.+)/);
|
||||
|
||||
return { icon: iconMap[type], href: target };
|
||||
|
||||
@@ -14,13 +14,13 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { FC } from 'react';
|
||||
import { useApi } from '@backstage/core';
|
||||
import { catalogApiRef } from '../../api/types';
|
||||
import { useAsync } from 'react-use';
|
||||
import { CircularProgress, useTheme } from '@material-ui/core';
|
||||
import React from 'react';
|
||||
import { useAsync } from 'react-use';
|
||||
import { catalogApiRef } from '../../plugin';
|
||||
|
||||
export const AllServicesCount: FC<{}> = () => {
|
||||
export const AllServicesCount = () => {
|
||||
const theme = useTheme();
|
||||
const catalogApi = useApi(catalogApiRef);
|
||||
const { value, loading } = useAsync(() => catalogApi.getEntities());
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { CatalogApi } from '@backstage/catalog-client';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import {
|
||||
ApiProvider,
|
||||
@@ -25,8 +26,8 @@ import {
|
||||
import { MockStorageApi, wrapInTestApp } from '@backstage/test-utils';
|
||||
import { fireEvent, render, waitFor } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { CatalogApi, catalogApiRef } from '../../api/types';
|
||||
import { EntityFilterGroupsProvider } from '../../filter';
|
||||
import { catalogApiRef } from '../../plugin';
|
||||
import { ButtonGroup, CatalogFilter } from './CatalogFilter';
|
||||
|
||||
describe('Catalog Filter', () => {
|
||||
|
||||
@@ -14,21 +14,21 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { CatalogApi } from '@backstage/catalog-client';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import {
|
||||
ApiProvider,
|
||||
ApiRegistry,
|
||||
IdentityApi,
|
||||
identityApiRef,
|
||||
storageApiRef,
|
||||
ProfileInfo,
|
||||
storageApiRef,
|
||||
} from '@backstage/core';
|
||||
import { MockStorageApi, wrapInTestApp } from '@backstage/test-utils';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { catalogApiRef } from '../..';
|
||||
import { CatalogApi } from '../../api/types';
|
||||
import { EntityFilterGroupsProvider } from '../../filter';
|
||||
import { catalogApiRef } from '../../plugin';
|
||||
import { CatalogPage } from './CatalogPage';
|
||||
|
||||
describe('CatalogPage', () => {
|
||||
|
||||
@@ -29,9 +29,9 @@ import SettingsIcon from '@material-ui/icons/Settings';
|
||||
import StarIcon from '@material-ui/icons/Star';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
import { catalogApiRef } from '../../api/types';
|
||||
import { EntityFilterGroupsProvider, useFilteredEntities } from '../../filter';
|
||||
import { useStarredEntities } from '../../hooks/useStarredEntities';
|
||||
import { catalogApiRef } from '../../plugin';
|
||||
import { ButtonGroup, CatalogFilter } from '../CatalogFilter/CatalogFilter';
|
||||
import { CatalogTable } from '../CatalogTable/CatalogTable';
|
||||
import { ResultsFilter } from '../ResultsFilter/ResultsFilter';
|
||||
|
||||
@@ -68,7 +68,13 @@ const columns: TableColumn<Entity>[] = [
|
||||
<>
|
||||
{entity.metadata.tags &&
|
||||
entity.metadata.tags.map(t => (
|
||||
<Chip key={t} label={t} style={{ marginBottom: '0px' }} />
|
||||
<Chip
|
||||
key={t}
|
||||
label={t}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
style={{ marginBottom: '0px' }}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
),
|
||||
@@ -150,10 +156,12 @@ export const CatalogTable = ({
|
||||
columns={columns}
|
||||
options={{
|
||||
paging: true,
|
||||
pageSize: 10,
|
||||
pageSize: 20,
|
||||
actionsColumnIndex: -1,
|
||||
loadingType: 'linear',
|
||||
showEmptyDataSourceMessage: !loading,
|
||||
padding: 'dense',
|
||||
pageSizeOptions: [20, 50, 100],
|
||||
}}
|
||||
title={`${titlePreamble} (${(entities && entities.length) || 0})`}
|
||||
data={entities}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { CatalogApi } from '@backstage/catalog-client';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import {
|
||||
ApiProvider,
|
||||
@@ -25,8 +26,8 @@ import {
|
||||
import { MockStorageApi, wrapInTestApp } from '@backstage/test-utils';
|
||||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { CatalogApi, catalogApiRef } from '../../api/types';
|
||||
import { EntityFilterGroupsProvider } from '../../filter';
|
||||
import { catalogApiRef } from '../../plugin';
|
||||
import { ResultsFilter } from './ResultsFilter';
|
||||
|
||||
describe('Results Filter', () => {
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import { Entity, LOCATION_ANNOTATION } from '@backstage/catalog-model';
|
||||
import { Progress, useApi, alertApiRef } from '@backstage/core';
|
||||
import { alertApiRef, Progress, useApi } from '@backstage/core';
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
@@ -31,7 +31,7 @@ import Alert from '@material-ui/lab/Alert';
|
||||
import React, { FC } from 'react';
|
||||
import { useAsync } from 'react-use';
|
||||
import { AsyncState } from 'react-use/lib/useAsync';
|
||||
import { catalogApiRef } from '../../api/types';
|
||||
import { catalogApiRef } from '../../plugin';
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
|
||||
@@ -18,7 +18,7 @@ import { Entity } from '@backstage/catalog-model';
|
||||
import { useApi } from '@backstage/core';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useAsyncFn } from 'react-use';
|
||||
import { catalogApiRef } from '../api/types';
|
||||
import { catalogApiRef } from '../plugin';
|
||||
import { filterGroupsContext, FilterGroupsContext } from './context';
|
||||
import {
|
||||
EntityFilterFn,
|
||||
|
||||
@@ -15,13 +15,13 @@
|
||||
*/
|
||||
|
||||
import { ApiProvider, ApiRegistry, storageApiRef } from '@backstage/core';
|
||||
import { MockStorageApi } from '@backstage/test-utils';
|
||||
import { act, renderHook } from '@testing-library/react-hooks';
|
||||
import React from 'react';
|
||||
import { catalogApiRef } from '../api/types';
|
||||
import { catalogApiRef } from '../plugin';
|
||||
import { EntityFilterGroupsProvider } from './EntityFilterGroupsProvider';
|
||||
import { FilterGroupStatesReady, FilterGroup } from './types';
|
||||
import { FilterGroup, FilterGroupStatesReady } from './types';
|
||||
import { useEntityFilterGroup } from './useEntityFilterGroup';
|
||||
import { MockStorageApi } from '@backstage/test-utils';
|
||||
|
||||
describe('useEntityFilterGroup', () => {
|
||||
let catalogApi: jest.Mocked<typeof catalogApiRef.T>;
|
||||
|
||||
@@ -18,8 +18,8 @@ import { errorApiRef, useApi } from '@backstage/core';
|
||||
import { createContext, useContext, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { useAsync } from 'react-use';
|
||||
import { catalogApiRef } from '../api/types';
|
||||
import { useEntityCompoundName } from '../components/useEntityCompoundName';
|
||||
import { catalogApiRef } from '../plugin';
|
||||
|
||||
type EntityLoadingStatus = {
|
||||
entity?: Entity;
|
||||
|
||||
@@ -14,12 +14,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { plugin } from './plugin';
|
||||
export * from './api/CatalogClient';
|
||||
export * from './api/types';
|
||||
export * from './routes';
|
||||
export { useEntityCompoundName } from './components/useEntityCompoundName';
|
||||
export { Router } from './components/Router';
|
||||
export { useEntity, EntityContext } from './hooks/useEntity';
|
||||
export * from '@backstage/catalog-client';
|
||||
export { AboutCard } from './components/AboutCard';
|
||||
export { EntityPageLayout } from './components/EntityPageLayout';
|
||||
export { Router } from './components/Router';
|
||||
export { useEntityCompoundName } from './components/useEntityCompoundName';
|
||||
export { EntityContext, useEntity } from './hooks/useEntity';
|
||||
export { catalogApiRef, plugin } from './plugin';
|
||||
export * from './routes';
|
||||
|
||||
@@ -14,13 +14,19 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { CatalogApi, CatalogClient } from '@backstage/catalog-client';
|
||||
import {
|
||||
createPlugin,
|
||||
createApiFactory,
|
||||
createApiRef,
|
||||
createPlugin,
|
||||
discoveryApiRef,
|
||||
} from '@backstage/core';
|
||||
import { catalogApiRef } from './api/types';
|
||||
import { CatalogClient } from './api/CatalogClient';
|
||||
|
||||
export const catalogApiRef = createApiRef<CatalogApi>({
|
||||
id: 'plugin.catalog.service',
|
||||
description:
|
||||
'Used by the Catalog plugin to make requests to accompanying backend',
|
||||
});
|
||||
|
||||
export const plugin = createPlugin({
|
||||
id: 'catalog',
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
# @backstage/plugin-circleci
|
||||
|
||||
## 0.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 28edd7d29: Create backend plugin through CLI
|
||||
- 30dd11122: Refactor to use DiscoveryApi
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [28edd7d29]
|
||||
- Updated dependencies [819a70229]
|
||||
- Updated dependencies [3a4236570]
|
||||
- Updated dependencies [ae5983387]
|
||||
- Updated dependencies [0d4459c08]
|
||||
- Updated dependencies [482b6313d]
|
||||
- Updated dependencies [e0be86b6f]
|
||||
- Updated dependencies [f70a52868]
|
||||
- Updated dependencies [12b5fe940]
|
||||
- Updated dependencies [368fd8243]
|
||||
- Updated dependencies [1c60f716e]
|
||||
- Updated dependencies [144c66d50]
|
||||
- Updated dependencies [a768a07fb]
|
||||
- Updated dependencies [b79017fd3]
|
||||
- Updated dependencies [6d97d2d6f]
|
||||
- Updated dependencies [5adfc005e]
|
||||
- Updated dependencies [f0aa01bcc]
|
||||
- Updated dependencies [0aecfded0]
|
||||
- Updated dependencies [93a3fa3ae]
|
||||
- Updated dependencies [782f3b354]
|
||||
- Updated dependencies [8b9c8196f]
|
||||
- Updated dependencies [2713f28f4]
|
||||
- Updated dependencies [406015b0d]
|
||||
- Updated dependencies [82759d3e4]
|
||||
- Updated dependencies [60d40892c]
|
||||
- Updated dependencies [ac8d5d5c7]
|
||||
- Updated dependencies [2ebcfac8d]
|
||||
- Updated dependencies [fa56f4615]
|
||||
- Updated dependencies [ebca83d48]
|
||||
- Updated dependencies [aca79334f]
|
||||
- Updated dependencies [c0d5242a0]
|
||||
- Updated dependencies [b3d57961c]
|
||||
- Updated dependencies [0b956f21b]
|
||||
- Updated dependencies [97c2cb19b]
|
||||
- Updated dependencies [3beb5c9fc]
|
||||
- Updated dependencies [754e31db5]
|
||||
- Updated dependencies [1611c6dbc]
|
||||
- @backstage/plugin-catalog@0.2.0
|
||||
- @backstage/core@0.2.0
|
||||
- @backstage/catalog-model@0.2.0
|
||||
- @backstage/theme@0.2.0
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-circleci",
|
||||
"version": "0.1.1-alpha.26",
|
||||
"version": "0.2.0",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -21,10 +21,10 @@
|
||||
"postpack": "backstage-cli postpack"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/catalog-model": "^0.1.1-alpha.26",
|
||||
"@backstage/core": "^0.1.1-alpha.26",
|
||||
"@backstage/plugin-catalog": "^0.1.1-alpha.26",
|
||||
"@backstage/theme": "^0.1.1-alpha.26",
|
||||
"@backstage/catalog-model": "^0.2.0",
|
||||
"@backstage/core": "^0.2.0",
|
||||
"@backstage/plugin-catalog": "^0.2.0",
|
||||
"@backstage/theme": "^0.2.0",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
@@ -38,9 +38,9 @@
|
||||
"react-use": "^15.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.26",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.26",
|
||||
"@backstage/test-utils": "^0.1.1-alpha.26",
|
||||
"@backstage/cli": "^0.2.0",
|
||||
"@backstage/dev-utils": "^0.1.2",
|
||||
"@backstage/test-utils": "^0.1.2",
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^10.4.1",
|
||||
"@testing-library/user-event": "^12.0.7",
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
# @backstage/plugin-cloudbuild
|
||||
|
||||
## 0.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 2846ef95c: Releasing Google Cloud Build Plugin
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [28edd7d29]
|
||||
- Updated dependencies [819a70229]
|
||||
- Updated dependencies [3a4236570]
|
||||
- Updated dependencies [ae5983387]
|
||||
- Updated dependencies [0d4459c08]
|
||||
- Updated dependencies [482b6313d]
|
||||
- Updated dependencies [e0be86b6f]
|
||||
- Updated dependencies [f70a52868]
|
||||
- Updated dependencies [12b5fe940]
|
||||
- Updated dependencies [368fd8243]
|
||||
- Updated dependencies [1c60f716e]
|
||||
- Updated dependencies [144c66d50]
|
||||
- Updated dependencies [a768a07fb]
|
||||
- Updated dependencies [b79017fd3]
|
||||
- Updated dependencies [6d97d2d6f]
|
||||
- Updated dependencies [5adfc005e]
|
||||
- Updated dependencies [f0aa01bcc]
|
||||
- Updated dependencies [0aecfded0]
|
||||
- Updated dependencies [93a3fa3ae]
|
||||
- Updated dependencies [782f3b354]
|
||||
- Updated dependencies [8b9c8196f]
|
||||
- Updated dependencies [2713f28f4]
|
||||
- Updated dependencies [406015b0d]
|
||||
- Updated dependencies [82759d3e4]
|
||||
- Updated dependencies [60d40892c]
|
||||
- Updated dependencies [ac8d5d5c7]
|
||||
- Updated dependencies [2ebcfac8d]
|
||||
- Updated dependencies [fa56f4615]
|
||||
- Updated dependencies [ebca83d48]
|
||||
- Updated dependencies [aca79334f]
|
||||
- Updated dependencies [c0d5242a0]
|
||||
- Updated dependencies [b3d57961c]
|
||||
- Updated dependencies [0b956f21b]
|
||||
- Updated dependencies [97c2cb19b]
|
||||
- Updated dependencies [3beb5c9fc]
|
||||
- Updated dependencies [754e31db5]
|
||||
- Updated dependencies [1611c6dbc]
|
||||
- @backstage/plugin-catalog@0.2.0
|
||||
- @backstage/core@0.2.0
|
||||
- @backstage/catalog-model@0.2.0
|
||||
- @backstage/theme@0.2.0
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-cloudbuild",
|
||||
"version": "0.1.1-alpha.26",
|
||||
"version": "0.2.0",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -20,10 +20,10 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/catalog-model": "^0.1.1-alpha.26",
|
||||
"@backstage/core": "^0.1.1-alpha.26",
|
||||
"@backstage/plugin-catalog": "^0.1.1-alpha.26",
|
||||
"@backstage/theme": "^0.1.1-alpha.26",
|
||||
"@backstage/catalog-model": "^0.2.0",
|
||||
"@backstage/core": "^0.2.0",
|
||||
"@backstage/plugin-catalog": "^0.2.0",
|
||||
"@backstage/theme": "^0.2.0",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
@@ -39,9 +39,9 @@
|
||||
"react-use": "^15.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.26",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.26",
|
||||
"@backstage/test-utils": "^0.1.1-alpha.26",
|
||||
"@backstage/cli": "^0.2.0",
|
||||
"@backstage/dev-utils": "^0.1.2",
|
||||
"@backstage/test-utils": "^0.1.2",
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^10.4.1",
|
||||
"@testing-library/user-event": "^12.0.7",
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
# @backstage/plugin-cost-insights
|
||||
|
||||
## 0.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- cab473771: This PR adds Spotify's Cost Insights Tool. Cost Insights explains costs from cloud services in an understandable way, using software terms familiar to your engineers. This tool helps you and your team make trade-offs between cost optimization efforts and your other priorities.
|
||||
|
||||
Cost Insights features:
|
||||
|
||||
Daily cost graph by team or billing account
|
||||
Cost comparison against configurable business metrics
|
||||
Insights panels for configurable cloud products your company uses
|
||||
Cost alerts and recommendations
|
||||
Selectable time periods for month over month, or quarter over quarter cost comparison
|
||||
Conversion of cost growth into average engineer cost (configurable) to help optimization trade-off decisions
|
||||
|
||||

|
||||
|
||||
This PR adds the Cost Insights frontend React plugin with a defined CostInsightsApi. We include an example client with static data in the expected format. This API should talk with a cloud billing backend that aggregates billing data from your cloud provider.
|
||||
|
||||
Fixes #688 💵
|
||||
|
||||
- bb48b9833: Added getLastCompleteBillingDate to the CostInsightsApi to reason about completeness of billing data
|
||||
- 6a84cb072: Enable custom alert types in Cost Insights
|
||||
- e7d4ac7ce: - getProjectDailyCost and getGroupDailyCost no longer accept a metric as a parameter
|
||||
- getDailyMetricData added to API for fetching daily metric data for given interval
|
||||
- dailyCost removed as configurable metric
|
||||
- default field added to metric configuration for displaying comparison metric data in top panel
|
||||
- Metric.kind can no longer be null
|
||||
- MetricData type added
|
||||
- 0e67c6b40: Remove product filters from query parameters
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 8d1360aa9: export test utilities for mocking context
|
||||
- 0ee9e9f66: migrate type utilities out of type definition files
|
||||
- 5c70f3d35: expose alerts utilities for export
|
||||
- fd8384d7e: prefer named exports
|
||||
- 26e69ab1a: Remove cost insights example client from demo app and export from plugin
|
||||
Create cost insights dev plugin using example client
|
||||
Make PluginConfig and dependent types public
|
||||
- Updated dependencies [819a70229]
|
||||
- Updated dependencies [ae5983387]
|
||||
- Updated dependencies [0d4459c08]
|
||||
- Updated dependencies [482b6313d]
|
||||
- Updated dependencies [1c60f716e]
|
||||
- Updated dependencies [144c66d50]
|
||||
- Updated dependencies [b79017fd3]
|
||||
- Updated dependencies [6d97d2d6f]
|
||||
- Updated dependencies [93a3fa3ae]
|
||||
- Updated dependencies [782f3b354]
|
||||
- Updated dependencies [2713f28f4]
|
||||
- Updated dependencies [406015b0d]
|
||||
- Updated dependencies [82759d3e4]
|
||||
- Updated dependencies [ac8d5d5c7]
|
||||
- Updated dependencies [ebca83d48]
|
||||
- Updated dependencies [aca79334f]
|
||||
- Updated dependencies [c0d5242a0]
|
||||
- Updated dependencies [3beb5c9fc]
|
||||
- Updated dependencies [754e31db5]
|
||||
- Updated dependencies [1611c6dbc]
|
||||
- @backstage/core@0.2.0
|
||||
- @backstage/theme@0.2.0
|
||||
- @backstage/test-utils@0.1.2
|
||||
@@ -21,7 +21,7 @@ yarn add @backstage/plugin-cost-insights
|
||||
|
||||
1. Configure `app-config.yaml`. See [Configuration](#configuration).
|
||||
|
||||
2. Create a CostInsights client. Clients must implement the CostInsightsApi interface. See the [API file](https://github.com/spotify/backstage/blob/master/plugins/cost-insights/src/api/CostInsightsApi.ts) for required methods and documentation.
|
||||
2. Create a CostInsights client. Clients must implement the CostInsightsApi interface. See the [API file](https://github.com/backstage/backstage/blob/master/plugins/cost-insights/src/api/CostInsightsApi.ts) for required methods and documentation.
|
||||
|
||||
```ts
|
||||
// path/to/CostInsightsClient.ts
|
||||
@@ -58,7 +58,7 @@ export { plugin as CostInsights } from '@backstage/plugin-cost-insights';
|
||||
|
||||
Cost Insights has only two required configuration fields: a map of cloud `products` for showing cost breakdowns and `engineerCost` - the average yearly cost of an engineer including benefits. Products must be defined as keys on the `products` field.
|
||||
|
||||
You can optionally supply a product `icon` to display in Cost Insights navigation. See the [type file](https://github.com/spotify/backstage/blob/master/plugins/cost-insights/src/types/Icon.ts) for supported types and Material UI icon [mappings](https://github.com/spotify/backstage/blob/master/plugins/cost-insights/src/utils/navigation.tsx).
|
||||
You can optionally supply a product `icon` to display in Cost Insights navigation. See the [type file](https://github.com/backstage/backstage/blob/master/plugins/cost-insights/src/types/Icon.ts) for supported types and Material UI icon [mappings](https://github.com/backstage/backstage/blob/master/plugins/cost-insights/src/utils/navigation.tsx).
|
||||
|
||||
**Note:** Product keys should be unique and camelCased. Backstage does not support underscores in configuration keys.
|
||||
|
||||
@@ -106,7 +106,7 @@ costInsights:
|
||||
|
||||
## Alerts
|
||||
|
||||
The CostInsightsApi `getAlerts` method may return any type of alert or recommendation (called collectively "Action Items" in Cost Insights) that implements the [Alert type](https://github.com/spotify/backstage/blob/master/plugins/cost-insights/src/types/Alert.ts). This allows you to deliver any alerts or recommendations specific to your infrastructure or company migrations.
|
||||
The CostInsightsApi `getAlerts` method may return any type of alert or recommendation (called collectively "Action Items" in Cost Insights) that implements the [Alert type](https://github.com/backstage/backstage/blob/master/plugins/cost-insights/src/types/Alert.ts). This allows you to deliver any alerts or recommendations specific to your infrastructure or company migrations.
|
||||
|
||||
The Alert type includes an `element` field to supply the JSX Element that will be rendered in the Cost Insights "Action Items" section; we recommend using Backstage's [InfoCard](https://backstage.io/storybook/?path=/story/layout-information-card--default) and [Recharts](http://recharts.org/en-US/) to show actionable visualizations.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-cost-insights",
|
||||
"version": "0.1.1-alpha.26",
|
||||
"version": "0.2.0",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -21,10 +21,10 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/config": "^0.1.1-alpha.26",
|
||||
"@backstage/core": "^0.1.1-alpha.26",
|
||||
"@backstage/test-utils": "^0.1.1-alpha.26",
|
||||
"@backstage/theme": "^0.1.1-alpha.26",
|
||||
"@backstage/config": "^0.1.1",
|
||||
"@backstage/core": "^0.2.0",
|
||||
"@backstage/test-utils": "^0.1.2",
|
||||
"@backstage/theme": "^0.2.0",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
@@ -46,9 +46,9 @@
|
||||
"yup": "^0.29.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.26",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.26",
|
||||
"@backstage/test-utils": "^0.1.1-alpha.26",
|
||||
"@backstage/cli": "^0.2.0",
|
||||
"@backstage/dev-utils": "^0.1.2",
|
||||
"@backstage/test-utils": "^0.1.2",
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^10.4.1",
|
||||
"@testing-library/user-event": "^12.0.7",
|
||||
|
||||
@@ -26,6 +26,34 @@ import {
|
||||
MetricData,
|
||||
} from '../types';
|
||||
|
||||
export type ProductInsightsOptions = {
|
||||
/**
|
||||
* The product from the cost-insights configuration in app-config.yaml
|
||||
*/
|
||||
product: string;
|
||||
|
||||
/**
|
||||
* The group id from getUserGroups or query parameters
|
||||
*/
|
||||
group: string;
|
||||
|
||||
/**
|
||||
* A time duration, such as P1M. See the Duration type for a detailed explanation
|
||||
* of how the durations are interpreted in Cost Insights.
|
||||
*/
|
||||
duration: Duration;
|
||||
|
||||
/**
|
||||
* The most current date for which billing data is complete, in YYYY-MM-DD format.
|
||||
*/
|
||||
lastCompleteBillingDate: string;
|
||||
|
||||
/**
|
||||
* (optional) The project id from getGroupProjects or query parameters
|
||||
*/
|
||||
project: Maybe<string>;
|
||||
};
|
||||
|
||||
export type CostInsightsApi = {
|
||||
/**
|
||||
* Get the most current date for which billing data is complete, in YYYY-MM-DD format. This helps
|
||||
@@ -108,19 +136,9 @@ export type CostInsightsApi = {
|
||||
* The time period is supplied as a Duration rather than intervals, since this is always expected
|
||||
* to return data for two bucketed time period (e.g. month vs month, or quarter vs quarter).
|
||||
*
|
||||
* @param product The product from the cost-insights configuration in app-config.yaml
|
||||
* @param group
|
||||
* @param duration A time duration, such as P1M. See the Duration type for a detailed explanation
|
||||
* of how the durations are interpreted in Cost Insights.
|
||||
* @param project (optional) The project id from getGroupProjects or query parameters
|
||||
* @param options Options to use when fetching insights for a particular cloud product and interval timeframe.
|
||||
*/
|
||||
getProductInsights(
|
||||
product: string,
|
||||
group: string,
|
||||
duration: Duration,
|
||||
project: Maybe<string>,
|
||||
): Promise<Entity>;
|
||||
|
||||
getProductInsights(options: ProductInsightsOptions): Promise<Entity>;
|
||||
/**
|
||||
* Get current cost alerts for a given group. These show up as Action Items for the group on the
|
||||
* Cost Insights page. Alerts may include cost-saving recommendations, such as infrastructure
|
||||
|
||||
+157
-165
@@ -17,7 +17,7 @@
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import regression, { DataPoint } from 'regression';
|
||||
import { CostInsightsApi } from '../src/api';
|
||||
import { CostInsightsApi, ProductInsightsOptions } from '../src/api';
|
||||
import {
|
||||
Alert,
|
||||
ChangeStatistic,
|
||||
@@ -27,7 +27,6 @@ import {
|
||||
Duration,
|
||||
Entity,
|
||||
Group,
|
||||
Maybe,
|
||||
MetricData,
|
||||
Project,
|
||||
ProjectGrowthData,
|
||||
@@ -194,176 +193,169 @@ export class ExampleCostInsightsClient implements CostInsightsApi {
|
||||
}
|
||||
|
||||
async getProductInsights(
|
||||
product: string,
|
||||
group: string,
|
||||
duration: Duration,
|
||||
project: Maybe<string>,
|
||||
productInsightsOptions: ProductInsightsOptions,
|
||||
): Promise<Entity> {
|
||||
const projectProductInsights = await this.request(
|
||||
{ product, group, duration, project },
|
||||
{
|
||||
id: 'project-product',
|
||||
aggregation: [80_000, 110_000],
|
||||
change: {
|
||||
ratio: 0.375,
|
||||
amount: 30_000,
|
||||
},
|
||||
entities: [
|
||||
{
|
||||
id: null, // entities with null ids will be appear as "Unlabeled" in product panels
|
||||
aggregation: [45_000, 50_000],
|
||||
change: {
|
||||
ratio: 0.111,
|
||||
amount: 5_000,
|
||||
},
|
||||
entities: [],
|
||||
},
|
||||
{
|
||||
id: 'entity-a',
|
||||
aggregation: [15_000, 20_000],
|
||||
change: {
|
||||
ratio: 0.333,
|
||||
amount: 5_000,
|
||||
},
|
||||
entities: [],
|
||||
},
|
||||
{
|
||||
id: 'entity-b',
|
||||
aggregation: [20_000, 30_000],
|
||||
change: {
|
||||
ratio: 0.5,
|
||||
amount: 10_000,
|
||||
},
|
||||
entities: [],
|
||||
},
|
||||
{
|
||||
id: 'entity-c',
|
||||
aggregation: [0, 10_000],
|
||||
change: {
|
||||
ratio: 10_000,
|
||||
amount: 10_000,
|
||||
},
|
||||
entities: [],
|
||||
},
|
||||
],
|
||||
const projectProductInsights = await this.request(productInsightsOptions, {
|
||||
id: 'project-product',
|
||||
aggregation: [80_000, 110_000],
|
||||
change: {
|
||||
ratio: 0.375,
|
||||
amount: 30_000,
|
||||
},
|
||||
);
|
||||
|
||||
const productInsights: Entity = await this.request(
|
||||
{ product, group, duration, project },
|
||||
{
|
||||
id: 'product',
|
||||
aggregation: [200_000, 250_000],
|
||||
change: {
|
||||
ratio: 0.2,
|
||||
amount: 50_000,
|
||||
entities: [
|
||||
{
|
||||
id: null, // entities with null ids will be appear as "Unlabeled" in product panels
|
||||
aggregation: [45_000, 50_000],
|
||||
change: {
|
||||
ratio: 0.111,
|
||||
amount: 5_000,
|
||||
},
|
||||
entities: [],
|
||||
},
|
||||
entities: [
|
||||
{
|
||||
id: null, // entities with null ids will be appear as "Unlabeled" in product panels
|
||||
aggregation: [36_000, 42_000],
|
||||
change: {
|
||||
ratio: 0.1666,
|
||||
amount: 6_000,
|
||||
},
|
||||
entities: [],
|
||||
{
|
||||
id: 'entity-a',
|
||||
aggregation: [15_000, 20_000],
|
||||
change: {
|
||||
ratio: 0.333,
|
||||
amount: 5_000,
|
||||
},
|
||||
{
|
||||
id: 'entity-a',
|
||||
aggregation: [15_000, 20_000],
|
||||
change: {
|
||||
ratio: -0.33333333,
|
||||
amount: 5_000,
|
||||
},
|
||||
entities: [],
|
||||
entities: [],
|
||||
},
|
||||
{
|
||||
id: 'entity-b',
|
||||
aggregation: [20_000, 30_000],
|
||||
change: {
|
||||
ratio: 0.5,
|
||||
amount: 10_000,
|
||||
},
|
||||
{
|
||||
id: 'entity-b',
|
||||
aggregation: [20_000, 30_000],
|
||||
change: {
|
||||
ratio: 0.5,
|
||||
amount: 10_000,
|
||||
},
|
||||
entities: [],
|
||||
entities: [],
|
||||
},
|
||||
{
|
||||
id: 'entity-c',
|
||||
aggregation: [0, 10_000],
|
||||
change: {
|
||||
ratio: 10_000,
|
||||
amount: 10_000,
|
||||
},
|
||||
{
|
||||
id: 'entity-c',
|
||||
aggregation: [18_000, 25_000],
|
||||
change: {
|
||||
ratio: 0.38,
|
||||
amount: 7_000,
|
||||
},
|
||||
entities: [],
|
||||
},
|
||||
{
|
||||
id: 'entity-d',
|
||||
aggregation: [15_000, 30_000],
|
||||
change: {
|
||||
ratio: 1,
|
||||
amount: 15_000,
|
||||
},
|
||||
entities: [],
|
||||
},
|
||||
{
|
||||
id: 'entity-e',
|
||||
aggregation: [0, 10_000],
|
||||
entities: [],
|
||||
change: {
|
||||
ratio: 10_000,
|
||||
amount: 10_000,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'entity-f',
|
||||
aggregation: [17_000, 19_000],
|
||||
change: {
|
||||
ratio: 0.118,
|
||||
amount: 2_000,
|
||||
},
|
||||
entities: [],
|
||||
},
|
||||
{
|
||||
id: 'entity-g',
|
||||
aggregation: [80_000, 60_000],
|
||||
change: {
|
||||
ratio: -0.25,
|
||||
amount: -20_000,
|
||||
},
|
||||
entities: [
|
||||
{
|
||||
id: 'vCPU Time Batch Belgium',
|
||||
aggregation: [15_000, 15_000],
|
||||
change: {
|
||||
ratio: 0,
|
||||
amount: 0,
|
||||
},
|
||||
entities: [],
|
||||
},
|
||||
{
|
||||
id: 'RAM Time Belgium',
|
||||
aggregation: [15_000, 30_000],
|
||||
change: {
|
||||
ratio: 1,
|
||||
amount: 15_000,
|
||||
},
|
||||
entities: [],
|
||||
},
|
||||
{
|
||||
id: 'Local Disk Time PD Standard Belgium',
|
||||
aggregation: [50_000, 15_000],
|
||||
change: {
|
||||
ratio: -0.7,
|
||||
amount: -35_000,
|
||||
},
|
||||
entities: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
entities: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return project ? projectProductInsights : productInsights;
|
||||
const productInsights: Entity = await this.request(productInsightsOptions, {
|
||||
id: 'product',
|
||||
aggregation: [200_000, 250_000],
|
||||
change: {
|
||||
ratio: 0.2,
|
||||
amount: 50_000,
|
||||
},
|
||||
entities: [
|
||||
{
|
||||
id: null, // entities with null ids will be appear as "Unlabeled" in product panels
|
||||
aggregation: [36_000, 42_000],
|
||||
change: {
|
||||
ratio: 0.1666,
|
||||
amount: 6_000,
|
||||
},
|
||||
entities: [],
|
||||
},
|
||||
{
|
||||
id: 'entity-a',
|
||||
aggregation: [15_000, 20_000],
|
||||
change: {
|
||||
ratio: -0.33333333,
|
||||
amount: 5_000,
|
||||
},
|
||||
entities: [],
|
||||
},
|
||||
{
|
||||
id: 'entity-b',
|
||||
aggregation: [20_000, 30_000],
|
||||
change: {
|
||||
ratio: 0.5,
|
||||
amount: 10_000,
|
||||
},
|
||||
entities: [],
|
||||
},
|
||||
{
|
||||
id: 'entity-c',
|
||||
aggregation: [18_000, 25_000],
|
||||
change: {
|
||||
ratio: 0.38,
|
||||
amount: 7_000,
|
||||
},
|
||||
entities: [],
|
||||
},
|
||||
{
|
||||
id: 'entity-d',
|
||||
aggregation: [15_000, 30_000],
|
||||
change: {
|
||||
ratio: 1,
|
||||
amount: 15_000,
|
||||
},
|
||||
entities: [],
|
||||
},
|
||||
{
|
||||
id: 'entity-e',
|
||||
aggregation: [0, 10_000],
|
||||
entities: [],
|
||||
change: {
|
||||
ratio: 10_000,
|
||||
amount: 10_000,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'entity-f',
|
||||
aggregation: [17_000, 19_000],
|
||||
change: {
|
||||
ratio: 0.118,
|
||||
amount: 2_000,
|
||||
},
|
||||
entities: [],
|
||||
},
|
||||
{
|
||||
id: 'entity-g',
|
||||
aggregation: [80_000, 60_000],
|
||||
change: {
|
||||
ratio: -0.25,
|
||||
amount: -20_000,
|
||||
},
|
||||
entities: [
|
||||
{
|
||||
id: 'vCPU Time Batch Belgium',
|
||||
aggregation: [15_000, 15_000],
|
||||
change: {
|
||||
ratio: 0,
|
||||
amount: 0,
|
||||
},
|
||||
entities: [],
|
||||
},
|
||||
{
|
||||
id: 'RAM Time Belgium',
|
||||
aggregation: [15_000, 30_000],
|
||||
change: {
|
||||
ratio: 1,
|
||||
amount: 15_000,
|
||||
},
|
||||
entities: [],
|
||||
},
|
||||
{
|
||||
id: 'Local Disk Time PD Standard Belgium',
|
||||
aggregation: [50_000, 15_000],
|
||||
change: {
|
||||
ratio: -0.7,
|
||||
amount: -35_000,
|
||||
},
|
||||
entities: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return productInsightsOptions.project
|
||||
? projectProductInsights
|
||||
: productInsights;
|
||||
}
|
||||
|
||||
async getAlerts(group: string): Promise<Alert[]> {
|
||||
|
||||
@@ -37,7 +37,10 @@ import {
|
||||
MetricData,
|
||||
CostInsightsTheme,
|
||||
} from '../../types';
|
||||
import { BarChartTooltip, BarChartTooltipItem } from '../BarChart';
|
||||
import {
|
||||
BarChartTooltip as Tooltip,
|
||||
BarChartTooltipItem as TooltipItem,
|
||||
} from '../BarChart';
|
||||
import {
|
||||
overviewGraphTickFormatter,
|
||||
formatGraphValue,
|
||||
@@ -120,11 +123,11 @@ export const CostOverviewChart = ({
|
||||
}));
|
||||
|
||||
return (
|
||||
<BarChartTooltip title={title}>
|
||||
<Tooltip title={title}>
|
||||
{items.map((item, index) => (
|
||||
<BarChartTooltipItem key={`${item.label}-${index}`} item={item} />
|
||||
<TooltipItem key={`${item.label}-${index}`} item={item} />
|
||||
))}
|
||||
</BarChartTooltip>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -17,10 +17,16 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { InfoCard, useApi } from '@backstage/core';
|
||||
import Alert from '@material-ui/lab/Alert';
|
||||
import { Typography } from '@material-ui/core';
|
||||
import { costInsightsApiRef } from '../../api';
|
||||
import { ProductInsightsChart } from './ProductInsightsChart';
|
||||
import { PeriodSelect } from '../PeriodSelect';
|
||||
import { useFilters, useLoading, useScroll } from '../../hooks';
|
||||
import {
|
||||
useFilters,
|
||||
useLastCompleteBillingDate,
|
||||
useLoading,
|
||||
useScroll,
|
||||
} from '../../hooks';
|
||||
import { useProductInsightsCardStyles as useStyles } from '../../utils/styles';
|
||||
import { mapFiltersToProps, mapLoadingToProps } from './selector';
|
||||
import { Duration, Maybe, Product, Entity } from '../../types';
|
||||
@@ -34,6 +40,7 @@ export const ProductInsightsCard = ({ product }: ProductInsightsCardProps) => {
|
||||
const client = useApi(costInsightsApiRef);
|
||||
const classes = useStyles();
|
||||
const { ScrollAnchor } = useScroll(product.kind);
|
||||
const lastCompleteBillingDate = useLastCompleteBillingDate();
|
||||
const [entity, setEntity] = useState<Maybe<Entity>>(null);
|
||||
const [error, setError] = useState<Maybe<Error>>(null);
|
||||
|
||||
@@ -49,22 +56,23 @@ export const ProductInsightsCard = ({ product }: ProductInsightsCardProps) => {
|
||||
const dispatchLoadingProduct = useCallback(dispatchLoading, [product.kind]);
|
||||
|
||||
const amount = entity?.entities?.length || 0;
|
||||
const hasCostsWithinTimeframe = entity?.change && !!amount;
|
||||
const hasCostsWithinTimeframe = !!(entity?.change && amount);
|
||||
|
||||
const subheader = amount
|
||||
const subheader = hasCostsWithinTimeframe
|
||||
? `${amount} ${pluralOf(amount, 'entity', 'entities')}, sorted by cost`
|
||||
: `There are no ${product.name} costs within this timeframe for your team's projects.`;
|
||||
: null;
|
||||
|
||||
useEffect(() => {
|
||||
async function load() {
|
||||
if (loadingProduct) {
|
||||
try {
|
||||
const e: Entity = await client.getProductInsights(
|
||||
product.kind,
|
||||
group!,
|
||||
productFilter!.duration,
|
||||
const e: Entity = await client.getProductInsights({
|
||||
product: product.kind,
|
||||
group: group!,
|
||||
duration: productFilter!.duration,
|
||||
lastCompleteBillingDate,
|
||||
project,
|
||||
);
|
||||
});
|
||||
setEntity(e);
|
||||
} catch (e) {
|
||||
setError(e);
|
||||
@@ -84,6 +92,7 @@ export const ProductInsightsCard = ({ product }: ProductInsightsCardProps) => {
|
||||
group,
|
||||
product.kind,
|
||||
project,
|
||||
lastCompleteBillingDate,
|
||||
]);
|
||||
|
||||
const onPeriodSelect = (duration: Duration) => {
|
||||
@@ -119,11 +128,17 @@ export const ProductInsightsCard = ({ product }: ProductInsightsCardProps) => {
|
||||
return (
|
||||
<InfoCard title={product.name} subheader={subheader} {...infoCardProps}>
|
||||
<ScrollAnchor behavior="smooth" top={-12} />
|
||||
{hasCostsWithinTimeframe && (
|
||||
{hasCostsWithinTimeframe ? (
|
||||
<ProductInsightsChart
|
||||
billingDate={lastCompleteBillingDate}
|
||||
duration={productFilter.duration}
|
||||
entity={entity}
|
||||
/>
|
||||
) : (
|
||||
<Typography>
|
||||
There are no {product.name} costs within this timeframe for your
|
||||
team's projects.
|
||||
</Typography>
|
||||
)}
|
||||
</InfoCard>
|
||||
);
|
||||
|
||||
@@ -49,17 +49,18 @@ import {
|
||||
import { BarChartOptions, Duration, Entity, Maybe } from '../../types';
|
||||
|
||||
export type ProductInsightsChartProps = {
|
||||
billingDate: string;
|
||||
entity: Entity;
|
||||
duration: Duration;
|
||||
};
|
||||
|
||||
export const ProductInsightsChart = ({
|
||||
billingDate,
|
||||
entity,
|
||||
duration,
|
||||
}: ProductInsightsChartProps) => {
|
||||
const classes = useStyles();
|
||||
const layoutClasses = useLayoutStyles();
|
||||
const lastCompleteBillingDate = useLastCompleteBillingDate();
|
||||
const [isOpen, setOpen] = useState(false);
|
||||
const [isClickable, setClickable] = useState(true);
|
||||
const [selectLabel, setSelected] = useState<Maybe<string>>(null);
|
||||
@@ -71,8 +72,8 @@ export const ProductInsightsChart = ({
|
||||
const resources = entity.entities.map(resourceOf);
|
||||
|
||||
const options: Partial<BarChartOptions> = {
|
||||
previousName: formatPeriod(duration, lastCompleteBillingDate, false),
|
||||
currentName: formatPeriod(duration, lastCompleteBillingDate, true),
|
||||
previousName: formatPeriod(duration, billingDate, false),
|
||||
currentName: formatPeriod(duration, billingDate, true),
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
+7
-9
@@ -20,7 +20,6 @@ import { Box, Typography } from '@material-ui/core';
|
||||
import { InfoCard } from '@backstage/core';
|
||||
import { AlertInstructionsLayout } from '../AlertInstructionsLayout';
|
||||
import { ProductInsightsChart } from '../ProductInsightsCard';
|
||||
import { MockBillingDateProvider } from '../../utils/tests';
|
||||
import {
|
||||
Alert,
|
||||
DEFAULT_DATE_FORMAT,
|
||||
@@ -172,14 +171,13 @@ export const ProjectGrowthInstructionsPage = () => {
|
||||
) that has grown in cost:
|
||||
</Typography>
|
||||
<Box mt={2} mb={2}>
|
||||
<MockBillingDateProvider lastCompleteBillingDate={today}>
|
||||
<InfoCard
|
||||
title={product.name}
|
||||
subheader="3 entities, sorted by cost"
|
||||
>
|
||||
<ProductInsightsChart duration={Duration.P3M} entity={entity} />
|
||||
</InfoCard>
|
||||
</MockBillingDateProvider>
|
||||
<InfoCard title={product.name} subheader="3 entities, sorted by cost">
|
||||
<ProductInsightsChart
|
||||
billingDate={today}
|
||||
duration={Duration.P3M}
|
||||
entity={entity}
|
||||
/>
|
||||
</InfoCard>
|
||||
</Box>
|
||||
<Typography paragraph>
|
||||
From here, you can dig into commit history or deployment logs to find
|
||||
|
||||
@@ -22,3 +22,4 @@ export { useCurrency } from './hooks';
|
||||
export * from './types';
|
||||
export * from './utils/tests';
|
||||
export * from './utils/duration';
|
||||
export * from './utils/alerts';
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
# @backstage/plugin-explore
|
||||
|
||||
## 0.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 28edd7d29: Create backend plugin through CLI
|
||||
- cab473771: This PR adds Spotify's Cost Insights Tool. Cost Insights explains costs from cloud services in an understandable way, using software terms familiar to your engineers. This tool helps you and your team make trade-offs between cost optimization efforts and your other priorities.
|
||||
|
||||
Cost Insights features:
|
||||
|
||||
Daily cost graph by team or billing account
|
||||
Cost comparison against configurable business metrics
|
||||
Insights panels for configurable cloud products your company uses
|
||||
Cost alerts and recommendations
|
||||
Selectable time periods for month over month, or quarter over quarter cost comparison
|
||||
Conversion of cost growth into average engineer cost (configurable) to help optimization trade-off decisions
|
||||
|
||||

|
||||
|
||||
This PR adds the Cost Insights frontend React plugin with a defined CostInsightsApi. We include an example client with static data in the expected format. This API should talk with a cloud billing backend that aggregates billing data from your cloud provider.
|
||||
|
||||
Fixes #688 💵
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [819a70229]
|
||||
- Updated dependencies [ae5983387]
|
||||
- Updated dependencies [0d4459c08]
|
||||
- Updated dependencies [482b6313d]
|
||||
- Updated dependencies [1c60f716e]
|
||||
- Updated dependencies [144c66d50]
|
||||
- Updated dependencies [b79017fd3]
|
||||
- Updated dependencies [6d97d2d6f]
|
||||
- Updated dependencies [93a3fa3ae]
|
||||
- Updated dependencies [782f3b354]
|
||||
- Updated dependencies [2713f28f4]
|
||||
- Updated dependencies [406015b0d]
|
||||
- Updated dependencies [82759d3e4]
|
||||
- Updated dependencies [ac8d5d5c7]
|
||||
- Updated dependencies [ebca83d48]
|
||||
- Updated dependencies [aca79334f]
|
||||
- Updated dependencies [c0d5242a0]
|
||||
- Updated dependencies [3beb5c9fc]
|
||||
- Updated dependencies [754e31db5]
|
||||
- Updated dependencies [1611c6dbc]
|
||||
- @backstage/core@0.2.0
|
||||
- @backstage/theme@0.2.0
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-explore",
|
||||
"version": "0.1.1-alpha.26",
|
||||
"version": "0.2.0",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -21,8 +21,8 @@
|
||||
"start": "backstage-cli plugin:serve"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/core": "^0.1.1-alpha.26",
|
||||
"@backstage/theme": "^0.1.1-alpha.26",
|
||||
"@backstage/core": "^0.2.0",
|
||||
"@backstage/theme": "^0.2.0",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
@@ -33,9 +33,9 @@
|
||||
"react-use": "^15.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.26",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.26",
|
||||
"@backstage/test-utils": "^0.1.1-alpha.26",
|
||||
"@backstage/cli": "^0.2.0",
|
||||
"@backstage/dev-utils": "^0.1.2",
|
||||
"@backstage/test-utils": "^0.1.2",
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^10.4.1",
|
||||
"@testing-library/user-event": "^12.0.7",
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
# @backstage/plugin-gcp-projects
|
||||
|
||||
## 0.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 28edd7d29: Create backend plugin through CLI
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [819a70229]
|
||||
- Updated dependencies [ae5983387]
|
||||
- Updated dependencies [0d4459c08]
|
||||
- Updated dependencies [482b6313d]
|
||||
- Updated dependencies [1c60f716e]
|
||||
- Updated dependencies [144c66d50]
|
||||
- Updated dependencies [b79017fd3]
|
||||
- Updated dependencies [6d97d2d6f]
|
||||
- Updated dependencies [93a3fa3ae]
|
||||
- Updated dependencies [782f3b354]
|
||||
- Updated dependencies [2713f28f4]
|
||||
- Updated dependencies [406015b0d]
|
||||
- Updated dependencies [82759d3e4]
|
||||
- Updated dependencies [ac8d5d5c7]
|
||||
- Updated dependencies [ebca83d48]
|
||||
- Updated dependencies [aca79334f]
|
||||
- Updated dependencies [c0d5242a0]
|
||||
- Updated dependencies [3beb5c9fc]
|
||||
- Updated dependencies [754e31db5]
|
||||
- Updated dependencies [1611c6dbc]
|
||||
- @backstage/core@0.2.0
|
||||
- @backstage/theme@0.2.0
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-gcp-projects",
|
||||
"version": "0.1.1-alpha.26",
|
||||
"version": "0.2.0",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -20,8 +20,8 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/core": "^0.1.1-alpha.26",
|
||||
"@backstage/theme": "^0.1.1-alpha.26",
|
||||
"@backstage/core": "^0.2.0",
|
||||
"@backstage/theme": "^0.2.0",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
@@ -31,9 +31,9 @@
|
||||
"react-use": "^15.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.26",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.26",
|
||||
"@backstage/test-utils": "^0.1.1-alpha.26",
|
||||
"@backstage/cli": "^0.2.0",
|
||||
"@backstage/dev-utils": "^0.1.2",
|
||||
"@backstage/test-utils": "^0.1.2",
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^10.4.1",
|
||||
"@testing-library/user-event": "^12.0.7",
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
# @backstage/plugin-github-actions
|
||||
|
||||
## 0.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 28edd7d29: Create backend plugin through CLI
|
||||
- d67c529ab: Adds a widget to show recent git workflow runs to the github actions plugin. The default setting is the last 5 runs across all branches but both branch and the number of runs are configurable.
|
||||
- 6d97d2d6f: The InfoCard variant `'height100'` is deprecated. Use variant `'gridItem'` instead.
|
||||
|
||||
When the InfoCard is displayed as a grid item within a grid, you may want items to have the same height for all items.
|
||||
Set to the `'gridItem'` variant to display the InfoCard with full height suitable for Grid:
|
||||
`<InfoCard variant="gridItem">...</InfoCard>`
|
||||
|
||||
Changed the InfoCards in '@backstage/plugin-github-actions', '@backstage/plugin-jenkins', '@backstage/plugin-lighthouse'
|
||||
to pass an optional variant to the corresponding card of the plugin.
|
||||
|
||||
As a result the overview content of the EntityPage shows cards with full height suitable for Grid.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [28edd7d29]
|
||||
- Updated dependencies [819a70229]
|
||||
- Updated dependencies [3a4236570]
|
||||
- Updated dependencies [ae5983387]
|
||||
- Updated dependencies [0d4459c08]
|
||||
- Updated dependencies [cbbd271c4]
|
||||
- Updated dependencies [482b6313d]
|
||||
- Updated dependencies [e0be86b6f]
|
||||
- Updated dependencies [f70a52868]
|
||||
- Updated dependencies [12b5fe940]
|
||||
- Updated dependencies [368fd8243]
|
||||
- Updated dependencies [1c60f716e]
|
||||
- Updated dependencies [144c66d50]
|
||||
- Updated dependencies [a768a07fb]
|
||||
- Updated dependencies [b79017fd3]
|
||||
- Updated dependencies [6d97d2d6f]
|
||||
- Updated dependencies [5adfc005e]
|
||||
- Updated dependencies [f0aa01bcc]
|
||||
- Updated dependencies [0aecfded0]
|
||||
- Updated dependencies [93a3fa3ae]
|
||||
- Updated dependencies [782f3b354]
|
||||
- Updated dependencies [8b9c8196f]
|
||||
- Updated dependencies [2713f28f4]
|
||||
- Updated dependencies [406015b0d]
|
||||
- Updated dependencies [82759d3e4]
|
||||
- Updated dependencies [60d40892c]
|
||||
- Updated dependencies [ac8d5d5c7]
|
||||
- Updated dependencies [2ebcfac8d]
|
||||
- Updated dependencies [fa56f4615]
|
||||
- Updated dependencies [ebca83d48]
|
||||
- Updated dependencies [aca79334f]
|
||||
- Updated dependencies [c0d5242a0]
|
||||
- Updated dependencies [b3d57961c]
|
||||
- Updated dependencies [0b956f21b]
|
||||
- Updated dependencies [26e69ab1a]
|
||||
- Updated dependencies [97c2cb19b]
|
||||
- Updated dependencies [3beb5c9fc]
|
||||
- Updated dependencies [cbab5bbf8]
|
||||
- Updated dependencies [754e31db5]
|
||||
- Updated dependencies [1611c6dbc]
|
||||
- @backstage/plugin-catalog@0.2.0
|
||||
- @backstage/core-api@0.2.0
|
||||
- @backstage/core@0.2.0
|
||||
- @backstage/catalog-model@0.2.0
|
||||
- @backstage/theme@0.2.0
|
||||
@@ -26,8 +26,7 @@ TBD
|
||||
name: backstage
|
||||
description: backstage.io
|
||||
annotations:
|
||||
github.com/project-slug: 'spotify/backstage'
|
||||
|
||||
github.com/project-slug: 'backstage/backstage'
|
||||
spec:
|
||||
type: website
|
||||
lifecycle: production
|
||||
|
||||
@@ -4,8 +4,8 @@ metadata:
|
||||
name: backstage
|
||||
description: backstage.io
|
||||
annotations:
|
||||
github.com/project-slug: 'spotify/backstage'
|
||||
backstage.io/techdocs-ref: github:https://github.com/spotify/backstage.git
|
||||
github.com/project-slug: 'backstage/backstage'
|
||||
backstage.io/techdocs-ref: github:https://github.com/backstage/backstage.git
|
||||
spec:
|
||||
type: website
|
||||
lifecycle: production
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-github-actions",
|
||||
"version": "0.1.1-alpha.26",
|
||||
"version": "0.2.0",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -21,11 +21,11 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/catalog-model": "^0.1.1-alpha.26",
|
||||
"@backstage/core": "^0.1.1-alpha.26",
|
||||
"@backstage/core-api": "^0.1.1-alpha.26",
|
||||
"@backstage/plugin-catalog": "^0.1.1-alpha.26",
|
||||
"@backstage/theme": "^0.1.1-alpha.26",
|
||||
"@backstage/catalog-model": "^0.2.0",
|
||||
"@backstage/core": "^0.2.0",
|
||||
"@backstage/core-api": "^0.2.0",
|
||||
"@backstage/plugin-catalog": "^0.2.0",
|
||||
"@backstage/theme": "^0.2.0",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
@@ -40,9 +40,9 @@
|
||||
"react-use": "^15.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.26",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.26",
|
||||
"@backstage/test-utils": "^0.1.1-alpha.26",
|
||||
"@backstage/cli": "^0.2.0",
|
||||
"@backstage/dev-utils": "^0.1.2",
|
||||
"@backstage/test-utils": "^0.1.2",
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^10.4.1",
|
||||
"@testing-library/user-event": "^12.0.7",
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
# @backstage/plugin-gitops-profiles
|
||||
|
||||
## 0.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 28edd7d29: Create backend plugin through CLI
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [819a70229]
|
||||
- Updated dependencies [ae5983387]
|
||||
- Updated dependencies [0d4459c08]
|
||||
- Updated dependencies [482b6313d]
|
||||
- Updated dependencies [1c60f716e]
|
||||
- Updated dependencies [144c66d50]
|
||||
- Updated dependencies [b79017fd3]
|
||||
- Updated dependencies [6d97d2d6f]
|
||||
- Updated dependencies [93a3fa3ae]
|
||||
- Updated dependencies [782f3b354]
|
||||
- Updated dependencies [2713f28f4]
|
||||
- Updated dependencies [406015b0d]
|
||||
- Updated dependencies [82759d3e4]
|
||||
- Updated dependencies [ac8d5d5c7]
|
||||
- Updated dependencies [ebca83d48]
|
||||
- Updated dependencies [aca79334f]
|
||||
- Updated dependencies [c0d5242a0]
|
||||
- Updated dependencies [3beb5c9fc]
|
||||
- Updated dependencies [754e31db5]
|
||||
- Updated dependencies [1611c6dbc]
|
||||
- @backstage/core@0.2.0
|
||||
- @backstage/theme@0.2.0
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-gitops-profiles",
|
||||
"version": "0.1.1-alpha.26",
|
||||
"version": "0.2.0",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -21,8 +21,8 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/core": "^0.1.1-alpha.26",
|
||||
"@backstage/theme": "^0.1.1-alpha.26",
|
||||
"@backstage/core": "^0.2.0",
|
||||
"@backstage/theme": "^0.2.0",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
@@ -32,9 +32,9 @@
|
||||
"react-use": "^15.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.26",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.26",
|
||||
"@backstage/test-utils": "^0.1.1-alpha.26",
|
||||
"@backstage/cli": "^0.2.0",
|
||||
"@backstage/dev-utils": "^0.1.2",
|
||||
"@backstage/test-utils": "^0.1.2",
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^10.4.1",
|
||||
"@testing-library/user-event": "^12.0.7",
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
# @backstage/plugin-graphiql
|
||||
|
||||
## 0.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 28edd7d29: Create backend plugin through CLI
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 782f3b354: add test case for Progress component
|
||||
- Updated dependencies [819a70229]
|
||||
- Updated dependencies [ae5983387]
|
||||
- Updated dependencies [0d4459c08]
|
||||
- Updated dependencies [482b6313d]
|
||||
- Updated dependencies [1c60f716e]
|
||||
- Updated dependencies [144c66d50]
|
||||
- Updated dependencies [b79017fd3]
|
||||
- Updated dependencies [6d97d2d6f]
|
||||
- Updated dependencies [93a3fa3ae]
|
||||
- Updated dependencies [782f3b354]
|
||||
- Updated dependencies [2713f28f4]
|
||||
- Updated dependencies [406015b0d]
|
||||
- Updated dependencies [82759d3e4]
|
||||
- Updated dependencies [ac8d5d5c7]
|
||||
- Updated dependencies [ebca83d48]
|
||||
- Updated dependencies [aca79334f]
|
||||
- Updated dependencies [c0d5242a0]
|
||||
- Updated dependencies [3beb5c9fc]
|
||||
- Updated dependencies [754e31db5]
|
||||
- Updated dependencies [1611c6dbc]
|
||||
- @backstage/core@0.2.0
|
||||
- @backstage/theme@0.2.0
|
||||
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"name": "@backstage/plugin-graphiql",
|
||||
"description": "Backstage plugin for browsing GraphQL APIs",
|
||||
"version": "0.1.1-alpha.26",
|
||||
"version": "0.2.0",
|
||||
"private": false,
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "dist/index.esm.js",
|
||||
"types": "dist/index.d.ts"
|
||||
},
|
||||
"homepage": "https://github.com/spotify/backstage/tree/master/plugins/graphiql#readme",
|
||||
"homepage": "https://github.com/backstage/backstage/tree/master/plugins/graphiql#readme",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/spotify/backstage",
|
||||
"url": "https://github.com/backstage/backstage",
|
||||
"directory": "plugins/graphiql"
|
||||
},
|
||||
"keywords": [
|
||||
@@ -31,8 +31,8 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/core": "^0.1.1-alpha.26",
|
||||
"@backstage/theme": "^0.1.1-alpha.26",
|
||||
"@backstage/core": "^0.2.0",
|
||||
"@backstage/theme": "^0.2.0",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
@@ -43,9 +43,9 @@
|
||||
"react-use": "^15.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.26",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.26",
|
||||
"@backstage/test-utils": "^0.1.1-alpha.26",
|
||||
"@backstage/cli": "^0.2.0",
|
||||
"@backstage/dev-utils": "^0.1.2",
|
||||
"@backstage/test-utils": "^0.1.2",
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^10.4.1",
|
||||
"@testing-library/user-event": "^12.0.7",
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# @backstage/plugin-graphql-backend
|
||||
|
||||
## 0.1.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [28edd7d29]
|
||||
- Updated dependencies [5249594c5]
|
||||
- Updated dependencies [56e4eb589]
|
||||
- Updated dependencies [e37c0a005]
|
||||
- Updated dependencies [f00ca3cb8]
|
||||
- Updated dependencies [6579769df]
|
||||
- Updated dependencies [8c2b76e45]
|
||||
- Updated dependencies [440a17b39]
|
||||
- Updated dependencies [8afce088a]
|
||||
- Updated dependencies [7bbeb049f]
|
||||
- @backstage/plugin-catalog-graphql@0.2.0
|
||||
- @backstage/backend-common@0.2.0
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-graphql-backend",
|
||||
"version": "0.1.1-alpha.26",
|
||||
"version": "0.1.2",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -19,9 +19,9 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-common": "^0.1.1-alpha.26",
|
||||
"@backstage/config": "^0.1.1-alpha.26",
|
||||
"@backstage/plugin-catalog-graphql": "^0.1.1-alpha.26",
|
||||
"@backstage/backend-common": "^0.2.0",
|
||||
"@backstage/config": "^0.1.1",
|
||||
"@backstage/plugin-catalog-graphql": "^0.2.0",
|
||||
"@graphql-modules/core": "^0.7.17",
|
||||
"@types/express": "^4.17.6",
|
||||
"apollo-server": "^2.16.1",
|
||||
@@ -35,7 +35,7 @@
|
||||
"yn": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.26",
|
||||
"@backstage/cli": "^0.2.0",
|
||||
"@types/supertest": "^2.0.8",
|
||||
"eslint-plugin-graphql": "^4.0.0",
|
||||
"msw": "^0.20.5",
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
# @backstage/plugin-jenkins
|
||||
|
||||
## 0.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 28edd7d29: Create backend plugin through CLI
|
||||
- 6d97d2d6f: The InfoCard variant `'height100'` is deprecated. Use variant `'gridItem'` instead.
|
||||
|
||||
When the InfoCard is displayed as a grid item within a grid, you may want items to have the same height for all items.
|
||||
Set to the `'gridItem'` variant to display the InfoCard with full height suitable for Grid:
|
||||
`<InfoCard variant="gridItem">...</InfoCard>`
|
||||
|
||||
Changed the InfoCards in '@backstage/plugin-github-actions', '@backstage/plugin-jenkins', '@backstage/plugin-lighthouse'
|
||||
to pass an optional variant to the corresponding card of the plugin.
|
||||
|
||||
As a result the overview content of the EntityPage shows cards with full height suitable for Grid.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 1297dcb3a: Refactor to use DiscoveryApi
|
||||
- Updated dependencies [28edd7d29]
|
||||
- Updated dependencies [819a70229]
|
||||
- Updated dependencies [3a4236570]
|
||||
- Updated dependencies [ae5983387]
|
||||
- Updated dependencies [0d4459c08]
|
||||
- Updated dependencies [482b6313d]
|
||||
- Updated dependencies [e0be86b6f]
|
||||
- Updated dependencies [f70a52868]
|
||||
- Updated dependencies [12b5fe940]
|
||||
- Updated dependencies [368fd8243]
|
||||
- Updated dependencies [1c60f716e]
|
||||
- Updated dependencies [144c66d50]
|
||||
- Updated dependencies [a768a07fb]
|
||||
- Updated dependencies [b79017fd3]
|
||||
- Updated dependencies [6d97d2d6f]
|
||||
- Updated dependencies [5adfc005e]
|
||||
- Updated dependencies [f0aa01bcc]
|
||||
- Updated dependencies [0aecfded0]
|
||||
- Updated dependencies [93a3fa3ae]
|
||||
- Updated dependencies [782f3b354]
|
||||
- Updated dependencies [8b9c8196f]
|
||||
- Updated dependencies [2713f28f4]
|
||||
- Updated dependencies [406015b0d]
|
||||
- Updated dependencies [82759d3e4]
|
||||
- Updated dependencies [60d40892c]
|
||||
- Updated dependencies [ac8d5d5c7]
|
||||
- Updated dependencies [2ebcfac8d]
|
||||
- Updated dependencies [fa56f4615]
|
||||
- Updated dependencies [ebca83d48]
|
||||
- Updated dependencies [aca79334f]
|
||||
- Updated dependencies [c0d5242a0]
|
||||
- Updated dependencies [b3d57961c]
|
||||
- Updated dependencies [0b956f21b]
|
||||
- Updated dependencies [97c2cb19b]
|
||||
- Updated dependencies [3beb5c9fc]
|
||||
- Updated dependencies [754e31db5]
|
||||
- Updated dependencies [1611c6dbc]
|
||||
- @backstage/plugin-catalog@0.2.0
|
||||
- @backstage/core@0.2.0
|
||||
- @backstage/catalog-model@0.2.0
|
||||
- @backstage/theme@0.2.0
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-jenkins",
|
||||
"version": "0.1.1-alpha.26",
|
||||
"version": "0.2.0",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -21,10 +21,10 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/catalog-model": "^0.1.1-alpha.26",
|
||||
"@backstage/core": "^0.1.1-alpha.26",
|
||||
"@backstage/plugin-catalog": "^0.1.1-alpha.26",
|
||||
"@backstage/theme": "^0.1.1-alpha.26",
|
||||
"@backstage/catalog-model": "^0.2.0",
|
||||
"@backstage/core": "^0.2.0",
|
||||
"@backstage/plugin-catalog": "^0.2.0",
|
||||
"@backstage/theme": "^0.2.0",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
@@ -36,9 +36,9 @@
|
||||
"react-use": "^15.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.26",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.26",
|
||||
"@backstage/test-utils": "^0.1.1-alpha.26",
|
||||
"@backstage/cli": "^0.2.0",
|
||||
"@backstage/dev-utils": "^0.1.2",
|
||||
"@backstage/test-utils": "^0.1.2",
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^10.4.1",
|
||||
"@testing-library/user-event": "^12.0.7",
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React, { FC } from 'react';
|
||||
import { Box, IconButton, Link, Typography } from '@material-ui/core';
|
||||
import { Box, IconButton, Link, Typography, Tooltip } from '@material-ui/core';
|
||||
import RetryIcon from '@material-ui/icons/Replay';
|
||||
import GitHubIcon from '@material-ui/icons/GitHub';
|
||||
import { generatePath, Link as RouterLink } from 'react-router-dom';
|
||||
@@ -179,9 +179,11 @@ const generatedColumns: TableColumn[] = [
|
||||
title: 'Actions',
|
||||
sorting: false,
|
||||
render: (row: Partial<CITableBuildInfo>) => (
|
||||
<IconButton onClick={row.onRestartClick}>
|
||||
<RetryIcon />
|
||||
</IconButton>
|
||||
<Tooltip title="Rerun build">
|
||||
<IconButton onClick={row.onRestartClick}>
|
||||
<RetryIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
),
|
||||
width: '10%',
|
||||
},
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# @backstage/plugin-kubernetes-backend
|
||||
|
||||
## 0.1.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [5249594c5]
|
||||
- Updated dependencies [56e4eb589]
|
||||
- Updated dependencies [e37c0a005]
|
||||
- Updated dependencies [f00ca3cb8]
|
||||
- Updated dependencies [6579769df]
|
||||
- Updated dependencies [8c2b76e45]
|
||||
- Updated dependencies [440a17b39]
|
||||
- Updated dependencies [8afce088a]
|
||||
- Updated dependencies [7bbeb049f]
|
||||
- @backstage/backend-common@0.2.0
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-kubernetes-backend",
|
||||
"version": "0.1.1-alpha.26",
|
||||
"version": "0.1.2",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -20,8 +20,8 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-common": "^0.1.1-alpha.26",
|
||||
"@backstage/config": "^0.1.1-alpha.26",
|
||||
"@backstage/backend-common": "^0.2.0",
|
||||
"@backstage/config": "^0.1.1",
|
||||
"@kubernetes/client-node": "^0.12.1",
|
||||
"@types/express": "^4.17.6",
|
||||
"compression": "^1.7.4",
|
||||
@@ -37,7 +37,7 @@
|
||||
"yn": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.26",
|
||||
"@backstage/cli": "^0.2.0",
|
||||
"supertest": "^4.0.2"
|
||||
},
|
||||
"files": [
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
# @backstage/plugin-kubernetes
|
||||
|
||||
## 0.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 28edd7d29: Create backend plugin through CLI
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [819a70229]
|
||||
- Updated dependencies [3a4236570]
|
||||
- Updated dependencies [ae5983387]
|
||||
- Updated dependencies [0d4459c08]
|
||||
- Updated dependencies [482b6313d]
|
||||
- Updated dependencies [e0be86b6f]
|
||||
- Updated dependencies [f70a52868]
|
||||
- Updated dependencies [12b5fe940]
|
||||
- Updated dependencies [1c60f716e]
|
||||
- Updated dependencies [144c66d50]
|
||||
- Updated dependencies [a768a07fb]
|
||||
- Updated dependencies [b79017fd3]
|
||||
- Updated dependencies [6d97d2d6f]
|
||||
- Updated dependencies [5adfc005e]
|
||||
- Updated dependencies [93a3fa3ae]
|
||||
- Updated dependencies [782f3b354]
|
||||
- Updated dependencies [2713f28f4]
|
||||
- Updated dependencies [406015b0d]
|
||||
- Updated dependencies [82759d3e4]
|
||||
- Updated dependencies [ac8d5d5c7]
|
||||
- Updated dependencies [fa56f4615]
|
||||
- Updated dependencies [ebca83d48]
|
||||
- Updated dependencies [aca79334f]
|
||||
- Updated dependencies [c0d5242a0]
|
||||
- Updated dependencies [b3d57961c]
|
||||
- Updated dependencies [3beb5c9fc]
|
||||
- Updated dependencies [754e31db5]
|
||||
- Updated dependencies [1611c6dbc]
|
||||
- @backstage/core@0.2.0
|
||||
- @backstage/catalog-model@0.2.0
|
||||
- @backstage/theme@0.2.0
|
||||
- @backstage/plugin-kubernetes-backend@0.1.2
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-kubernetes",
|
||||
"version": "0.1.1-alpha.26",
|
||||
"version": "0.2.0",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -20,11 +20,11 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/catalog-model": "^0.1.1-alpha.26",
|
||||
"@backstage/config": "^0.1.1-alpha.26",
|
||||
"@backstage/core": "^0.1.1-alpha.26",
|
||||
"@backstage/plugin-kubernetes-backend": "^0.1.1-alpha.26",
|
||||
"@backstage/theme": "^0.1.1-alpha.26",
|
||||
"@backstage/catalog-model": "^0.2.0",
|
||||
"@backstage/config": "^0.1.1",
|
||||
"@backstage/core": "^0.2.0",
|
||||
"@backstage/plugin-kubernetes-backend": "^0.1.2",
|
||||
"@backstage/theme": "^0.2.0",
|
||||
"@kubernetes/client-node": "^0.12.1",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
@@ -35,9 +35,9 @@
|
||||
"react-use": "^15.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.26",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.26",
|
||||
"@backstage/test-utils": "^0.1.1-alpha.26",
|
||||
"@backstage/cli": "^0.2.0",
|
||||
"@backstage/dev-utils": "^0.1.2",
|
||||
"@backstage/test-utils": "^0.1.2",
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^10.4.1",
|
||||
"@testing-library/user-event": "^12.0.7",
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
# @backstage/plugin-lighthouse
|
||||
|
||||
## 0.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 28edd7d29: Create backend plugin through CLI
|
||||
- 6d97d2d6f: The InfoCard variant `'height100'` is deprecated. Use variant `'gridItem'` instead.
|
||||
|
||||
When the InfoCard is displayed as a grid item within a grid, you may want items to have the same height for all items.
|
||||
Set to the `'gridItem'` variant to display the InfoCard with full height suitable for Grid:
|
||||
`<InfoCard variant="gridItem">...</InfoCard>`
|
||||
|
||||
Changed the InfoCards in '@backstage/plugin-github-actions', '@backstage/plugin-jenkins', '@backstage/plugin-lighthouse'
|
||||
to pass an optional variant to the corresponding card of the plugin.
|
||||
|
||||
As a result the overview content of the EntityPage shows cards with full height suitable for Grid.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [28edd7d29]
|
||||
- Updated dependencies [819a70229]
|
||||
- Updated dependencies [3a4236570]
|
||||
- Updated dependencies [ae5983387]
|
||||
- Updated dependencies [0d4459c08]
|
||||
- Updated dependencies [cbbd271c4]
|
||||
- Updated dependencies [482b6313d]
|
||||
- Updated dependencies [e0be86b6f]
|
||||
- Updated dependencies [f70a52868]
|
||||
- Updated dependencies [12b5fe940]
|
||||
- Updated dependencies [368fd8243]
|
||||
- Updated dependencies [1c60f716e]
|
||||
- Updated dependencies [144c66d50]
|
||||
- Updated dependencies [a768a07fb]
|
||||
- Updated dependencies [b79017fd3]
|
||||
- Updated dependencies [6d97d2d6f]
|
||||
- Updated dependencies [5adfc005e]
|
||||
- Updated dependencies [f0aa01bcc]
|
||||
- Updated dependencies [0aecfded0]
|
||||
- Updated dependencies [93a3fa3ae]
|
||||
- Updated dependencies [782f3b354]
|
||||
- Updated dependencies [8b9c8196f]
|
||||
- Updated dependencies [2713f28f4]
|
||||
- Updated dependencies [406015b0d]
|
||||
- Updated dependencies [82759d3e4]
|
||||
- Updated dependencies [60d40892c]
|
||||
- Updated dependencies [ac8d5d5c7]
|
||||
- Updated dependencies [2ebcfac8d]
|
||||
- Updated dependencies [fa56f4615]
|
||||
- Updated dependencies [ebca83d48]
|
||||
- Updated dependencies [aca79334f]
|
||||
- Updated dependencies [c0d5242a0]
|
||||
- Updated dependencies [b3d57961c]
|
||||
- Updated dependencies [0b956f21b]
|
||||
- Updated dependencies [26e69ab1a]
|
||||
- Updated dependencies [97c2cb19b]
|
||||
- Updated dependencies [3beb5c9fc]
|
||||
- Updated dependencies [cbab5bbf8]
|
||||
- Updated dependencies [754e31db5]
|
||||
- Updated dependencies [1611c6dbc]
|
||||
- @backstage/plugin-catalog@0.2.0
|
||||
- @backstage/core-api@0.2.0
|
||||
- @backstage/core@0.2.0
|
||||
- @backstage/catalog-model@0.2.0
|
||||
- @backstage/theme@0.2.0
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user