chore: resolve conflicts

This commit is contained in:
Marvin9
2020-10-28 10:51:36 +05:30
219 changed files with 1702 additions and 2102 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend': patch
---
Fix `CatalogBuilder#addProcessor`.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-backend': patch
---
Added .fromConfig static factories for Preparers and Publishers + read integrations config to support url location types
@@ -0,0 +1,5 @@
---
'@backstage/plugin-cost-insights': patch
---
prefer named exports
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend': minor
---
Removed the parseData step from catalog processors. Locations readers should emit full entities instead.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend': minor
---
Removed support for deprecated `catalog.providers` config that have been moved to `integrations`
+9
View File
@@ -0,0 +1,9 @@
---
'example-app': patch
'@backstage/core-api': patch
'@backstage/plugin-cost-insights': patch
---
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
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog': patch
---
update the EntityNotFound component
+10
View File
@@ -0,0 +1,10 @@
---
'@backstage/plugin-scaffolder-backend': patch
---
Added support for configuring the working directory of the Scaffolder:
```yaml
backend:
workingDirectory: /some-dir # Use this to configure a working directory for the scaffolder, defaults to the OS temp-dir
```
+5
View File
@@ -0,0 +1,5 @@
---
'example-app': patch
---
cleaning up because external plugins have already implemented new api for creating
+1
View File
@@ -3,6 +3,7 @@
registry "https://registry.npmjs.org/"
disable-self-update-check true
lastUpdateCheck 1580389148099
yarn-path ".yarn/releases/yarn-1.22.1.js"
network-timeout 600000
+4 -2
View File
@@ -6,6 +6,10 @@ If you encounter issues while upgrading to a newer version, don't hesitate to re
## Next Release
> Collect changes for the next release below
## v0.1.1-alpha.26
### @backstage/cli
- Configuration files are no longer selected through `APP_ENV` or `NODE_ENV`. The default configuration files are `app-config.yaml` and, fix it exists, `app-config.local.yaml` in the repo root. To load a different set of files, use `--config <path>` arguments.
@@ -16,8 +20,6 @@ If you encounter issues while upgrading to a newer version, don't hesitate to re
## v0.1.1-alpha.25
> Collect changes for the next release below
### @backstage/cli
- The recommended way to set the configuration environment is now to use `APP_ENV` instead of `NODE_ENV`.
+1 -1
View File
@@ -16,6 +16,7 @@ backend:
credentials: true
csp:
connect-src: ["'self'", 'http:', 'https:']
# workingDirectory: /tmp # Use this to configure a working direcotry for the scaffolder, defaults to the OS temp-dir
# See README.md in the proxy-backend plugin for information on the configuration format
proxy:
@@ -156,7 +157,6 @@ catalog:
scaffolder:
github:
host: https://github.com
token:
$env: GITHUB_TOKEN
visibility: public # or 'internal' or 'private'
@@ -0,0 +1,58 @@
# Running the backend behind a Corporate Proxy
Let's admit it, we've all been there. Sometimes you've gotta run stuff with no way out to the public internet, only the smallest of corporate proxy tunnels.
Whilst this isn't supported natively by Backstage, this might help you get your installation up and running making calls through the said proxy tunnel.
Unfortunately, `nodejs` does not respect `HTTP(S)_PROXY` environment variables by default, and the library that we use to provide `fetch` functionality `node-fetch` (provided by `cross-fetch`) does not also respect these environment variables.
There are however some ways to get this to work without too much effort. It's most likely that you're going to run into these issues from the `backend` part of `backstage` as that's the part that isn't helped by your browser or OS's settings for the corporate proxy.
**Note:** You're gonna want to be in your backend working directory for these solutions as that's where the requests come from that don't go through this proxy.
### Using `global-agent`
1. Install `global-agent` using `yarn install global-agent`
2. Go to the entry file for the backend (`src/index.ts`)
3. At the top of the file paste the following:
```ts
import 'global-agent/bootstrap';
```
4. Start the backend with the `global-agent` variables
```sh
export GLOBAL_AGENT_HTTP_PROXY=$HTTP_PROXY
yarn start
```
More information and more options for configuring `global-agent` including just using the default environment variables can be found here: https://github.com/gajus/global-agent
### Using `proxy-agent`
`proxy-agent` is a library that you can use to override the `globalAgents` of `node` land with a tunnel to use for each request.
1. Install `proxy-agent` using `yarn install proxy-agent`
2. Go to the entry file for the backend (`src/index.ts`)
3. At the top of the file paste the following:
```ts
import ProxyAgent from 'proxy-agent';
import http from 'http';
import https from 'https';
/*
Something to note here, this might need different configuration depending on your own setup.
If you only have an http_proxy then you'll need to set that as both the http and https globalAgent instead.
*/
if (process.env.HTTP_PROXY) {
http.globalAgent = new ProxyAgent(process.env.HTTP_PROXY);
}
if (process.env.HTTPS_PROXY) {
https.globalAgent = new ProxyAgent(process.env.HTTPS_PROXY);
}
```
4. Start the backend with `yarn start`
@@ -111,44 +111,8 @@ export default async function createPlugin({
templaters.register('cookiecutter', cookiecutterTemplater);
templaters.register('cra', craTemplater);
const filePreparer = new FilePreparer();
const githubPreparer = new GithubPreparer();
const gitlabPreparer = new GitlabPreparer(config);
const preparers = new Preparers();
preparers.register('file', filePreparer);
preparers.register('github', githubPreparer);
preparers.register('gitlab', gitlabPreparer);
preparers.register('gitlab/api', gitlabPreparer);
const publishers = new Publishers();
const githubToken = config.getString('scaffolder.github.token');
const repoVisibility = config.getString(
'scaffolder.github.visibility',
) as RepoVisibilityOptions;
const githubClient = new Octokit({ auth: githubToken });
const githubPublisher = new GithubPublisher({
client: githubClient,
token: githubToken,
repoVisibility,
});
publishers.register('file', githubPublisher);
publishers.register('github', githubPublisher);
const gitLabConfig = config.getOptionalConfig('scaffolder.gitlab.api');
if (gitLabConfig) {
const gitLabToken = gitLabConfig.getString('token');
const gitLabClient = new Gitlab({
host: gitLabConfig.getOptionalString('baseUrl'),
token: gitLabToken,
});
const gitLabPublisher = new GitlabPublisher(gitLabClient, gitLabToken);
publishers.register('gitlab', gitLabPublisher);
publishers.register('gitlab/api', gitLabPublisher);
}
const preparers = await Preparers.fromConfig(config, { logger });
const publishers = await Publishers.fromConfig(config, { logger });
const dockerClient = new Docker();
return await createRouter({
+1 -1
View File
@@ -2,5 +2,5 @@
"packages": ["packages/*", "plugins/*"],
"npmClient": "yarn",
"useWorkspaces": true,
"version": "0.1.1-alpha.25"
"version": "0.1.1-alpha.26"
}
+1 -1
View File
@@ -14,7 +14,7 @@
"rename-version": "docusaurus-rename-version"
},
"devDependencies": {
"@spotify/prettier-config": "^8.0.0",
"@spotify/prettier-config": "^9.0.0",
"docusaurus": "^2.0.0-alpha.66",
"js-yaml": "^3.14.0",
"prettier": "^2.0.5"
+4 -4
View File
@@ -959,10 +959,10 @@
resolved "https://registry.npmjs.org/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd"
integrity sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow==
"@spotify/prettier-config@^8.0.0":
version "8.0.0"
resolved "https://registry.npmjs.org/@spotify/prettier-config/-/prettier-config-8.0.0.tgz#8b6c2bd579ddc54887155a0721fe04e96c89f7f2"
integrity sha512-so8w32ZV42CHWxOEXcBtbNO/hLXFrQNXVmhfzhUI6dVB9cq2xjRaiqu8GjFj8LvKbWpPj+S+KwTIS4aDVWqrFQ==
"@spotify/prettier-config@^9.0.0":
version "9.0.0"
resolved "https://registry.yarnpkg.com/@spotify/prettier-config/-/prettier-config-9.0.0.tgz#7b562d56573c6fc0094446fbc92b22bc318945dc"
integrity sha512-In1q0tIiqTYKAGe3KOHDcFDdZRFISyQeSeipeTHGfki23ebHRZcjxvqj5SSdBkw65D4VpSREMi0s9i5iJiMcTw==
"@types/cheerio@^0.22.8":
version "0.22.21"
+29 -32
View File
@@ -1,43 +1,42 @@
{
"name": "example-app",
"version": "0.1.1-alpha.25",
"version": "0.1.1-alpha.26",
"private": true,
"bundled": true,
"dependencies": {
"@backstage/catalog-model": "^0.1.1-alpha.25",
"@backstage/cli": "^0.1.1-alpha.25",
"@backstage/core": "^0.1.1-alpha.25",
"@backstage/plugin-api-docs": "^0.1.1-alpha.25",
"@backstage/plugin-catalog": "^0.1.1-alpha.25",
"@backstage/plugin-circleci": "^0.1.1-alpha.25",
"@backstage/plugin-cloudbuild": "^0.1.1-alpha.25",
"@backstage/plugin-cost-insights": "^0.1.1-alpha.25",
"@backstage/plugin-explore": "^0.1.1-alpha.25",
"@backstage/plugin-gcp-projects": "^0.1.1-alpha.25",
"@backstage/plugin-github-actions": "^0.1.1-alpha.25",
"@backstage/plugin-gitops-profiles": "^0.1.1-alpha.25",
"@backstage/plugin-graphiql": "^0.1.1-alpha.25",
"@backstage/plugin-jenkins": "^0.1.1-alpha.25",
"@backstage/plugin-kubernetes": "^0.1.1-alpha.25",
"@backstage/plugin-lighthouse": "^0.1.1-alpha.25",
"@backstage/plugin-newrelic": "^0.1.1-alpha.25",
"@backstage/plugin-register-component": "^0.1.1-alpha.25",
"@backstage/plugin-rollbar": "^0.1.1-alpha.25",
"@backstage/plugin-scaffolder": "^0.1.1-alpha.25",
"@backstage/plugin-sentry": "^0.1.1-alpha.25",
"@backstage/plugin-tech-radar": "^0.1.1-alpha.25",
"@backstage/plugin-techdocs": "^0.1.1-alpha.25",
"@backstage/plugin-user-settings": "^0.1.1-alpha.25",
"@backstage/plugin-welcome": "^0.1.1-alpha.25",
"@backstage/test-utils": "^0.1.1-alpha.25",
"@backstage/theme": "^0.1.1-alpha.25",
"@backstage/catalog-model": "^0.1.1-alpha.26",
"@backstage/cli": "^0.1.1-alpha.26",
"@backstage/core": "^0.1.1-alpha.26",
"@backstage/plugin-api-docs": "^0.1.1-alpha.26",
"@backstage/plugin-catalog": "^0.1.1-alpha.26",
"@backstage/plugin-circleci": "^0.1.1-alpha.26",
"@backstage/plugin-cloudbuild": "^0.1.1-alpha.26",
"@backstage/plugin-cost-insights": "^0.1.1-alpha.26",
"@backstage/plugin-explore": "^0.1.1-alpha.26",
"@backstage/plugin-gcp-projects": "^0.1.1-alpha.26",
"@backstage/plugin-github-actions": "^0.1.1-alpha.26",
"@backstage/plugin-gitops-profiles": "^0.1.1-alpha.26",
"@backstage/plugin-graphiql": "^0.1.1-alpha.26",
"@backstage/plugin-jenkins": "^0.1.1-alpha.26",
"@backstage/plugin-kubernetes": "^0.1.1-alpha.26",
"@backstage/plugin-lighthouse": "^0.1.1-alpha.26",
"@backstage/plugin-newrelic": "^0.1.1-alpha.26",
"@backstage/plugin-register-component": "^0.1.1-alpha.26",
"@backstage/plugin-rollbar": "^0.1.1-alpha.26",
"@backstage/plugin-scaffolder": "^0.1.1-alpha.26",
"@backstage/plugin-sentry": "^0.1.1-alpha.26",
"@backstage/plugin-tech-radar": "^0.1.1-alpha.26",
"@backstage/plugin-techdocs": "^0.1.1-alpha.26",
"@backstage/plugin-user-settings": "^0.1.1-alpha.26",
"@backstage/plugin-welcome": "^0.1.1-alpha.26",
"@backstage/test-utils": "^0.1.1-alpha.26",
"@backstage/theme": "^0.1.1-alpha.26",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@octokit/rest": "^18.0.0",
"@roadiehq/backstage-plugin-github-insights": "^0.2.7",
"@roadiehq/backstage-plugin-github-pull-requests": "^0.5.2",
"@roadiehq/backstage-plugin-travis-ci": "^0.2.3",
"dayjs": "^1.9.1",
"@roadiehq/backstage-plugin-travis-ci": "^0.2.5",
"history": "^5.0.0",
"prop-types": "^15.7.2",
"react": "^16.12.0",
@@ -46,7 +45,6 @@
"react-router": "6.0.0-beta.0",
"react-router-dom": "6.0.0-beta.0",
"react-use": "^15.3.3",
"regression": "^2.0.1",
"zen-observable": "^0.8.15"
},
"devDependencies": {
@@ -58,7 +56,6 @@
"@types/jquery": "^3.3.34",
"@types/node": "^12.0.0",
"@types/react-dom": "^16.9.8",
"@types/regression": "^2.0.0",
"@types/zen-observable": "^0.8.0",
"cross-env": "^7.0.0",
"cypress": "^4.2.0",
+3 -15
View File
@@ -26,17 +26,9 @@ import {
} from '@backstage/plugin-graphiql';
import {
TravisCIApi,
travisCIApiRef,
} from '@roadiehq/backstage-plugin-travis-ci';
import {
GithubPullRequestsClient,
githubPullRequestsApiRef,
} from '@roadiehq/backstage-plugin-github-pull-requests';
import { costInsightsApiRef } from '@backstage/plugin-cost-insights';
import { ExampleCostInsightsClient } from './plugins/cost-insights';
costInsightsApiRef,
ExampleCostInsightsClient,
} from '@backstage/plugin-cost-insights';
export const apis = [
createApiFactory({
@@ -59,8 +51,4 @@ export const apis = [
}),
createApiFactory(costInsightsApiRef, new ExampleCostInsightsClient()),
// TODO: move to plugins
createApiFactory(travisCIApiRef, new TravisCIApi()),
createApiFactory(githubPullRequestsApiRef, new GithubPullRequestsClient()),
];
+6 -6
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/backend-common",
"description": "Common functionality library for Backstage backends",
"version": "0.1.1-alpha.25",
"version": "0.1.1-alpha.26",
"main": "src/index.ts",
"types": "src/index.ts",
"private": false,
@@ -29,10 +29,10 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/cli-common": "^0.1.1-alpha.25",
"@backstage/config": "^0.1.1-alpha.25",
"@backstage/config-loader": "^0.1.1-alpha.25",
"@backstage/test-utils": "^0.1.1-alpha.25",
"@backstage/cli-common": "^0.1.1-alpha.26",
"@backstage/config": "^0.1.1-alpha.26",
"@backstage/config-loader": "^0.1.1-alpha.26",
"@backstage/test-utils": "^0.1.1-alpha.26",
"@types/cors": "^2.8.6",
"@types/express": "^4.17.6",
"compression": "^1.7.4",
@@ -62,7 +62,7 @@
}
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.25",
"@backstage/cli": "^0.1.1-alpha.26",
"@types/compression": "^1.7.0",
"@types/http-errors": "^1.6.3",
"@types/minimist": "^1.2.0",
+16 -16
View File
@@ -1,6 +1,6 @@
{
"name": "example-backend",
"version": "0.1.1-alpha.25",
"version": "0.1.1-alpha.26",
"main": "dist/index.cjs.js",
"types": "src/index.ts",
"private": true,
@@ -18,24 +18,24 @@
"migrate:create": "knex migrate:make -x ts"
},
"dependencies": {
"@backstage/backend-common": "^0.1.1-alpha.25",
"@backstage/catalog-model": "^0.1.1-alpha.25",
"@backstage/config": "^0.1.1-alpha.25",
"@backstage/plugin-app-backend": "^0.1.1-alpha.25",
"@backstage/plugin-auth-backend": "^0.1.1-alpha.25",
"@backstage/plugin-catalog-backend": "^0.1.1-alpha.25",
"@backstage/plugin-graphql-backend": "^0.1.1-alpha.25",
"@backstage/plugin-kubernetes-backend": "^0.1.1-alpha.25",
"@backstage/plugin-proxy-backend": "^0.1.1-alpha.25",
"@backstage/plugin-rollbar-backend": "^0.1.1-alpha.25",
"@backstage/plugin-scaffolder-backend": "^0.1.1-alpha.25",
"@backstage/plugin-sentry-backend": "^0.1.1-alpha.25",
"@backstage/plugin-techdocs-backend": "^0.1.1-alpha.25",
"@backstage/backend-common": "^0.1.1-alpha.26",
"@backstage/catalog-model": "^0.1.1-alpha.26",
"@backstage/config": "^0.1.1-alpha.26",
"@backstage/plugin-app-backend": "^0.1.1-alpha.26",
"@backstage/plugin-auth-backend": "^0.1.1-alpha.26",
"@backstage/plugin-catalog-backend": "^0.1.1-alpha.26",
"@backstage/plugin-graphql-backend": "^0.1.1-alpha.26",
"@backstage/plugin-kubernetes-backend": "^0.1.1-alpha.26",
"@backstage/plugin-proxy-backend": "^0.1.1-alpha.26",
"@backstage/plugin-rollbar-backend": "^0.1.1-alpha.26",
"@backstage/plugin-scaffolder-backend": "^0.1.1-alpha.26",
"@backstage/plugin-sentry-backend": "^0.1.1-alpha.26",
"@backstage/plugin-techdocs-backend": "^0.1.1-alpha.26",
"@gitbeaker/node": "^23.5.0",
"@octokit/rest": "^18.0.0",
"azure-devops-node-api": "^10.1.1",
"dockerode": "^3.2.0",
"example-app": "^0.1.1-alpha.25",
"example-app": "^0.1.1-alpha.26",
"express": "^4.17.1",
"express-promise-router": "^3.0.3",
"knex": "^0.21.1",
@@ -45,7 +45,7 @@
"winston": "^3.2.1"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.25",
"@backstage/cli": "^0.1.1-alpha.26",
"@types/dockerode": "^2.5.32",
"@types/express": "^4.17.6",
"@types/express-serve-static-core": "^4.17.5",
+4 -114
View File
@@ -17,22 +17,11 @@
import {
CookieCutter,
createRouter,
FilePreparer,
GithubPreparer,
GitlabPreparer,
AzurePreparer,
Preparers,
Publishers,
GithubPublisher,
GitlabPublisher,
AzurePublisher,
CreateReactAppTemplater,
Templaters,
RepoVisibilityOptions,
} from '@backstage/plugin-scaffolder-backend';
import { Octokit } from '@octokit/rest';
import { Gitlab } from '@gitbeaker/node';
import { getPersonalAccessTokenHandler, WebApi } from 'azure-devops-node-api';
import type { PluginEnvironment } from '../types';
import Docker from 'dockerode';
@@ -46,116 +35,17 @@ export default async function createPlugin({
templaters.register('cookiecutter', cookiecutterTemplater);
templaters.register('cra', craTemplater);
const filePreparer = new FilePreparer();
const gitlabPreparer = new GitlabPreparer(config);
const azurePreparer = new AzurePreparer(config);
const preparers = new Preparers();
preparers.register('file', filePreparer);
preparers.register('gitlab', gitlabPreparer);
preparers.register('gitlab/api', gitlabPreparer);
preparers.register('azure/api', azurePreparer);
const publishers = new Publishers();
const githubConfig = config.getOptionalConfig('scaffolder.github');
if (githubConfig) {
try {
const repoVisibility = githubConfig.getString(
'visibility',
) as RepoVisibilityOptions;
const githubToken = githubConfig.getString('token');
const githubHost =
githubConfig.getOptionalString('host') ?? 'https://github.com';
const githubClient = new Octokit({
auth: githubToken,
baseUrl: githubHost,
});
const githubPublisher = new GithubPublisher({
client: githubClient,
token: githubToken,
repoVisibility,
});
const githubPreparer = new GithubPreparer({ token: githubToken });
preparers.register('github', githubPreparer);
publishers.register('file', githubPublisher);
publishers.register('github', githubPublisher);
} catch (e) {
const providerName = 'github';
if (process.env.NODE_ENV !== 'development') {
throw new Error(
`Failed to initialize ${providerName} scaffolding provider, ${e.message}`,
);
}
logger.warn(
`Skipping ${providerName} scaffolding provider, ${e.message}`,
);
}
}
const gitLabConfig = config.getOptionalConfig('scaffolder.gitlab.api');
if (gitLabConfig) {
try {
const gitLabToken = gitLabConfig.getString('token');
const gitLabClient = new Gitlab({
host: gitLabConfig.getOptionalString('baseUrl'),
token: gitLabToken,
});
const gitLabPublisher = new GitlabPublisher(gitLabClient, gitLabToken);
publishers.register('gitlab', gitLabPublisher);
publishers.register('gitlab/api', gitLabPublisher);
} catch (e) {
const providerName = 'gitlab';
if (process.env.NODE_ENV !== 'development') {
throw new Error(
`Failed to initialize ${providerName} scaffolding provider, ${e.message}`,
);
}
logger.warn(
`Skipping ${providerName} scaffolding provider, ${e.message}`,
);
}
}
const azureConfig = config.getOptionalConfig('scaffolder.azure');
if (azureConfig) {
try {
const baseUrl = azureConfig.getString('baseUrl');
const azureToken = azureConfig.getConfig('api').getString('token');
const authHandler = getPersonalAccessTokenHandler(azureToken);
const webApi = new WebApi(baseUrl, authHandler);
const azureClient = await webApi.getGitApi();
const azurePublisher = new AzurePublisher(azureClient, azureToken);
publishers.register('azure/api', azurePublisher);
} catch (e) {
const providerName = 'azure';
if (process.env.NODE_ENV !== 'development') {
throw new Error(
`Failed to initialize ${providerName} scaffolding provider, ${e.message}`,
);
}
logger.warn(
`Skipping ${providerName} scaffolding provider, ${e.message}`,
);
}
}
const preparers = await Preparers.fromConfig(config, { logger });
const publishers = await Publishers.fromConfig(config, { logger });
const dockerClient = new Docker();
return await createRouter({
preparers,
templaters,
publishers,
logger,
config,
dockerClient,
});
}
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/catalog-model",
"version": "0.1.1-alpha.25",
"version": "0.1.1-alpha.26",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -20,7 +20,7 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/config": "^0.1.1-alpha.25",
"@backstage/config": "^0.1.1-alpha.26",
"@types/json-schema": "^7.0.5",
"@types/yup": "^0.29.8",
"json-schema": "^0.2.5",
@@ -29,7 +29,7 @@
"yup": "^0.29.3"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.25",
"@backstage/cli": "^0.1.1-alpha.26",
"@types/express": "^4.17.6",
"@types/jest": "^26.0.7",
"@types/lodash": "^4.14.151",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/cli-common",
"description": "Common functionality used by cli, backend, and create-app",
"version": "0.1.1-alpha.25",
"version": "0.1.1-alpha.26",
"private": false,
"main": "src/index.ts",
"types": "src/index.ts",
+5 -5
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/cli",
"description": "CLI for developing Backstage plugins and apps",
"version": "0.1.1-alpha.25",
"version": "0.1.1-alpha.26",
"private": false,
"publishConfig": {
"access": "public"
@@ -28,13 +28,13 @@
"backstage-cli": "bin/backstage-cli"
},
"dependencies": {
"@backstage/cli-common": "^0.1.1-alpha.25",
"@backstage/config": "^0.1.1-alpha.25",
"@backstage/config-loader": "^0.1.1-alpha.25",
"@backstage/cli-common": "^0.1.1-alpha.26",
"@backstage/config": "^0.1.1-alpha.26",
"@backstage/config-loader": "^0.1.1-alpha.26",
"@hot-loader/react-dom": "^16.13.0",
"@lerna/package-graph": "^3.18.5",
"@lerna/project": "^3.18.0",
"@rollup/plugin-commonjs": "^13.0.0",
"@rollup/plugin-commonjs": "^16.0.0",
"@rollup/plugin-json": "^4.0.2",
"@rollup/plugin-node-resolve": "^9.0.0",
"@rollup/plugin-yaml": "^2.1.1",
+1 -5
View File
@@ -40,17 +40,13 @@ const fileHandlers = [
patterns: ['package.json'],
handler: handlers.packageJson,
},
{
patterns: ['tsconfig.json'],
handler: handlers.exactMatch,
},
{
// make sure files in 1st level of src/ and dev/ exist
patterns: ['.eslintrc.js', /^(src|dev)\/[^/]+$/],
handler: handlers.exists,
},
{
patterns: ['README.md', /^src\//],
patterns: ['README.md', 'tsconfig.json', /^src\//],
handler: handlers.skip,
},
];
+6
View File
@@ -20,6 +20,7 @@ import handlebars from 'handlebars';
import ora from 'ora';
import { basename, dirname } from 'path';
import recursive from 'recursive-readdir';
import { paths } from './paths';
const TASK_NAME_MAX_LENGTH = 14;
@@ -72,6 +73,7 @@ export async function templatingTask(
const files = await recursive(templateDir).catch(error => {
throw new Error(`Failed to read template directory: ${error.message}`);
});
const isMonoRepo = await fs.pathExists(paths.resolveTargetRoot('lerna.json'));
for (const file of files) {
const destinationFile = file.replace(templateDir, destinationDir);
@@ -92,6 +94,10 @@ export async function templatingTask(
});
});
} else {
if (isMonoRepo && file.match('tsconfig.json')) {
continue;
}
await Task.forItem('copying', basename(file), async () => {
await fs.copyFile(file, destinationFile).catch(error => {
const destination = destinationFile;
@@ -0,0 +1,13 @@
{
"extends": "@backstage/cli/config/tsconfig.json",
"include": [
"src",
"dev",
"migrations"
],
"exclude": ["node_modules"],
"compilerOptions": {
"outDir": "dist-types",
"rootDir": "."
}
}
@@ -0,0 +1,12 @@
{
"extends": "@backstage/cli/config/tsconfig.json",
"include": [
"src",
"dev"
],
"exclude": ["node_modules"],
"compilerOptions": {
"outDir": "dist-types",
"rootDir": "."
}
}
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/config-loader",
"description": "Config loading functionality used by Backstage backend, and CLI",
"version": "0.1.1-alpha.25",
"version": "0.1.1-alpha.26",
"private": false,
"publishConfig": {
"access": "public",
@@ -30,7 +30,7 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/config": "^0.1.1-alpha.25",
"@backstage/config": "^0.1.1-alpha.26",
"fs-extra": "^9.0.0",
"yaml": "^1.9.2",
"yup": "^0.29.3"
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/config",
"description": "Config API used by Backstage core, backend, and CLI",
"version": "0.1.1-alpha.25",
"version": "0.1.1-alpha.26",
"private": false,
"publishConfig": {
"access": "public",
+6 -6
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/core-api",
"description": "Internal Core API used by Backstage plugins and apps",
"version": "0.1.1-alpha.25",
"version": "0.1.1-alpha.26",
"private": false,
"publishConfig": {
"access": "public",
@@ -29,9 +29,9 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/config": "^0.1.1-alpha.25",
"@backstage/test-utils": "^0.1.1-alpha.25",
"@backstage/theme": "^0.1.1-alpha.25",
"@backstage/config": "^0.1.1-alpha.26",
"@backstage/test-utils": "^0.1.1-alpha.26",
"@backstage/theme": "^0.1.1-alpha.26",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@types/react": "^16.9",
@@ -42,8 +42,8 @@
"zen-observable": "^0.8.15"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.25",
"@backstage/test-utils-core": "^0.1.1-alpha.25",
"@backstage/cli": "^0.1.1-alpha.26",
"@backstage/test-utils-core": "^0.1.1-alpha.26",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
+1 -41
View File
@@ -14,50 +14,10 @@
* limitations under the License.
*/
import { ComponentType } from 'react';
import {
PluginOutput,
RoutePath,
RouteOptions,
FeatureFlagName,
BackstagePlugin,
} from './types';
import { PluginConfig, PluginOutput, BackstagePlugin } from './types';
import { validateBrowserCompat, validateFlagName } from '../app/FeatureFlags';
import { RouteRef } from '../routing';
import { AnyApiFactory } from '../apis';
export type PluginConfig = {
id: string;
apis?: Iterable<AnyApiFactory>;
register?(hooks: PluginHooks): void;
};
export type PluginHooks = {
router: RouterHooks;
featureFlags: FeatureFlagsHooks;
};
export type RouterHooks = {
addRoute(
target: RouteRef,
Component: ComponentType<any>,
options?: RouteOptions,
): void;
/**
* @deprecated See the `addRoute` method
*/
registerRoute(
path: RoutePath,
Component: ComponentType<any>,
options?: RouteOptions,
): void;
};
export type FeatureFlagsHooks = {
register(name: FeatureFlagName): void;
};
export class PluginImpl {
private storedOutput?: PluginOutput[];
+32
View File
@@ -73,3 +73,35 @@ export type BackstagePlugin = {
output(): PluginOutput[];
getApis(): Iterable<AnyApiFactory>;
};
export type PluginConfig = {
id: string;
apis?: Iterable<AnyApiFactory>;
register?(hooks: PluginHooks): void;
};
export type PluginHooks = {
router: RouterHooks;
featureFlags: FeatureFlagsHooks;
};
export type RouterHooks = {
addRoute(
target: RouteRef,
Component: ComponentType<any>,
options?: RouteOptions,
): void;
/**
* @deprecated See the `addRoute` method
*/
registerRoute(
path: RoutePath,
Component: ComponentType<any>,
options?: RouteOptions,
): void;
};
export type FeatureFlagsHooks = {
register(name: FeatureFlagName): void;
};
+6 -6
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/core",
"description": "Core API used by Backstage plugins and apps",
"version": "0.1.1-alpha.25",
"version": "0.1.1-alpha.26",
"private": false,
"publishConfig": {
"access": "public",
@@ -29,9 +29,9 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/config": "^0.1.1-alpha.25",
"@backstage/core-api": "^0.1.1-alpha.25",
"@backstage/theme": "^0.1.1-alpha.25",
"@backstage/config": "^0.1.1-alpha.26",
"@backstage/core-api": "^0.1.1-alpha.26",
"@backstage/theme": "^0.1.1-alpha.26",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
@@ -55,8 +55,8 @@
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.25",
"@backstage/test-utils": "^0.1.1-alpha.25",
"@backstage/cli": "^0.1.1-alpha.26",
"@backstage/test-utils": "^0.1.1-alpha.26",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/create-app",
"description": "Create app package for Backstage",
"version": "0.1.1-alpha.25",
"version": "0.1.1-alpha.26",
"private": false,
"publishConfig": {
"access": "public"
@@ -27,7 +27,7 @@
"start": "nodemon --"
},
"dependencies": {
"@backstage/cli-common": "^0.1.1-alpha.25",
"@backstage/cli-common": "^0.1.1-alpha.26",
"chalk": "^4.0.0",
"commander": "^6.1.0",
"fs-extra": "^9.0.0",
@@ -38,6 +38,18 @@ backend:
#ca: # if you have a CA file and want to verify it you can uncomment this section
# $file: <file-path>/ca/server.crt
{{/if}}
# workingDirectory: /tmp # Use this to configure a working direcotry for the scaffolder, defaults to the OS temp-dir
integrations:
github:
- host: github.com
token:
$env: GITHUB_TOKEN
### Example for how to add your GitHub Enterprise instance using the API:
# - host: ghe.example.net
# apiBaseUrl: https://ghe.example.net/api/v3
# token:
# $env: GHE_TOKEN
proxy:
'/test':
@@ -66,17 +78,6 @@ scaffolder:
catalog:
rules:
- allow: [Component, API, Group, User, Template, Location]
processors:
github:
providers:
- target: https://github.com
token:
$env: GITHUB_TOKEN
# Example for how to add your GitHub Enterprise instance:
# - target: https://ghe.example.net
# apiBaseUrl: https://ghe.example.net/api/v3
# token:
# $env: GHE_TOKEN
locations:
# Backstage example components
- type: url
@@ -102,6 +102,7 @@ export default async function createPlugin({
templaters,
publishers,
logger,
config,
dockerClient,
});
}
+5 -5
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/dev-utils",
"description": "Utilities for developing Backstage plugins.",
"version": "0.1.1-alpha.25",
"version": "0.1.1-alpha.26",
"private": false,
"publishConfig": {
"access": "public",
@@ -29,10 +29,10 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/cli": "^0.1.1-alpha.25",
"@backstage/core": "^0.1.1-alpha.25",
"@backstage/test-utils": "^0.1.1-alpha.25",
"@backstage/theme": "^0.1.1-alpha.25",
"@backstage/cli": "^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",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@testing-library/jest-dom": "^5.10.1",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "docgen",
"description": "Tool for generating API Documentation for itself",
"version": "0.1.1-alpha.25",
"version": "0.1.1-alpha.26",
"private": true,
"homepage": "https://backstage.io",
"repository": {
+3 -3
View File
@@ -1,7 +1,7 @@
{
"name": "e2e-test",
"description": "E2E test for verifying Backstage packages",
"version": "0.1.1-alpha.25",
"version": "0.1.1-alpha.26",
"private": true,
"homepage": "https://backstage.io",
"repository": {
@@ -24,14 +24,14 @@
"e2e-test": "bin/e2e-test"
},
"devDependencies": {
"@backstage/cli-common": "^0.1.1-alpha.25",
"@backstage/cli-common": "^0.1.1-alpha.26",
"@types/fs-extra": "^9.0.1",
"@types/node": "^13.7.2",
"chalk": "^4.0.0",
"commander": "^6.1.0",
"cross-fetch": "^3.0.6",
"fs-extra": "^9.0.0",
"handlebars": "^4.7.3",
"cross-fetch": "^3.0.6",
"pgtools": "^0.3.0",
"tree-kill": "^1.2.2",
"ts-node": "^8.6.2",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "storybook",
"version": "0.1.1-alpha.25",
"version": "0.1.1-alpha.26",
"description": "Storybook build for core package",
"private": true,
"scripts": {
@@ -14,7 +14,7 @@
]
},
"dependencies": {
"@backstage/theme": "^0.1.1-alpha.25"
"@backstage/theme": "^0.1.1-alpha.26"
},
"devDependencies": {
"@storybook/addon-actions": "^6.0.21",
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@techdocs/cli",
"description": "CLI for running TechDocs locally.",
"version": "0.1.1-alpha.25",
"version": "0.1.1-alpha.26",
"private": false,
"publishConfig": {
"access": "public"
@@ -40,7 +40,7 @@
"ext": "ts"
},
"dependencies": {
"@backstage/cli": "^0.1.1-alpha.25",
"@backstage/cli": "^0.1.1-alpha.26",
"commander": "^6.1.0",
"fs-extra": "^9.0.1",
"http-proxy": "^1.18.1",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/test-utils-core",
"description": "Utilities to test Backstage core",
"version": "0.1.1-alpha.25",
"version": "0.1.1-alpha.26",
"private": false,
"publishConfig": {
"access": "public",
+5 -5
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/test-utils",
"description": "Utilities to test Backstage plugins and apps.",
"version": "0.1.1-alpha.25",
"version": "0.1.1-alpha.26",
"private": false,
"publishConfig": {
"access": "public",
@@ -29,10 +29,10 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/cli": "^0.1.1-alpha.25",
"@backstage/core-api": "^0.1.1-alpha.25",
"@backstage/test-utils-core": "^0.1.1-alpha.25",
"@backstage/theme": "^0.1.1-alpha.25",
"@backstage/cli": "^0.1.1-alpha.26",
"@backstage/core-api": "^0.1.1-alpha.26",
"@backstage/test-utils-core": "^0.1.1-alpha.26",
"@backstage/theme": "^0.1.1-alpha.26",
"@material-ui/core": "^4.11.0",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/theme",
"description": "material-ui theme for use with Backstage.",
"version": "0.1.1-alpha.25",
"version": "0.1.1-alpha.26",
"private": false,
"publishConfig": {
"access": "public",
@@ -31,7 +31,7 @@
"@material-ui/core": "^4.11.0"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.25"
"@backstage/cli": "^0.1.1-alpha.26"
},
"files": [
"dist"
+10 -10
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-api-docs",
"version": "0.1.1-alpha.25",
"version": "0.1.1-alpha.26",
"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.25",
"@backstage/core": "^0.1.1-alpha.25",
"@backstage/plugin-catalog": "^0.1.1-alpha.25",
"@backstage/theme": "^0.1.1-alpha.25",
"@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",
"@material-icons/font": "^1.0.2",
"@material-ui/core": "^4.11.0",
@@ -39,9 +39,9 @@
"swagger-ui-react": "^3.31.1"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.25",
"@backstage/dev-utils": "^0.1.1-alpha.25",
"@backstage/test-utils": "^0.1.1-alpha.25",
"@backstage/cli": "^0.1.1-alpha.26",
"@backstage/dev-utils": "^0.1.1-alpha.26",
"@backstage/test-utils": "^0.1.1-alpha.26",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
@@ -49,8 +49,8 @@
"@types/node": "^12.0.0",
"@types/react": "^16.9",
"@types/swagger-ui-react": "^3.23.3",
"msw": "^0.21.2",
"cross-fetch": "^3.0.6"
"cross-fetch": "^3.0.6",
"msw": "^0.21.2"
},
"files": [
"dist"
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-app-backend",
"version": "0.1.1-alpha.25",
"version": "0.1.1-alpha.26",
"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.25",
"@backstage/config-loader": "^0.1.1-alpha.25",
"@backstage/backend-common": "^0.1.1-alpha.26",
"@backstage/config-loader": "^0.1.1-alpha.26",
"@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.25",
"@backstage/cli": "^0.1.1-alpha.26",
"@types/supertest": "^2.0.8",
"msw": "^0.20.5",
"supertest": "^4.0.2"
+6 -6
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-auth-backend",
"version": "0.1.1-alpha.25",
"version": "0.1.1-alpha.26",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -20,13 +20,14 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/backend-common": "^0.1.1-alpha.25",
"@backstage/catalog-model": "^0.1.1-alpha.25",
"@backstage/config": "^0.1.1-alpha.25",
"@backstage/backend-common": "^0.1.1-alpha.26",
"@backstage/catalog-model": "^0.1.1-alpha.26",
"@backstage/config": "^0.1.1-alpha.26",
"@types/express": "^4.17.6",
"compression": "^1.7.4",
"cookie-parser": "^1.4.5",
"cors": "^2.8.5",
"cross-fetch": "^3.0.6",
"express": "^4.17.1",
"express-promise-router": "^3.0.3",
"fs-extra": "^9.0.0",
@@ -47,11 +48,10 @@
"passport-saml": "^1.3.3",
"uuid": "^8.0.0",
"winston": "^3.2.1",
"cross-fetch": "^3.0.6",
"yn": "^4.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.25",
"@backstage/cli": "^0.1.1-alpha.26",
"@types/body-parser": "^1.19.0",
"@types/cookie-parser": "^1.4.2",
"@types/jwt-decode": "2.2.1",
+6 -6
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-catalog-backend",
"version": "0.1.1-alpha.25",
"version": "0.1.1-alpha.26",
"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.25",
"@backstage/catalog-model": "^0.1.1-alpha.25",
"@backstage/config": "^0.1.1-alpha.25",
"@backstage/backend-common": "^0.1.1-alpha.26",
"@backstage/catalog-model": "^0.1.1-alpha.26",
"@backstage/config": "^0.1.1-alpha.26",
"@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.25",
"@backstage/test-utils": "^0.1.1-alpha.25",
"@backstage/cli": "^0.1.1-alpha.26",
"@backstage/test-utils": "^0.1.1-alpha.26",
"@types/core-js": "^2.5.4",
"@types/git-url-parse": "^9.0.0",
"@types/ldapjs": "^1.0.9",
@@ -28,7 +28,6 @@ import { CatalogRulesEnforcer } from './CatalogRules';
import * as result from './processors/results';
import {
CatalogProcessor,
CatalogProcessorDataResult,
CatalogProcessorEmit,
CatalogProcessorEntityResult,
CatalogProcessorErrorResult,
@@ -75,8 +74,6 @@ export class LocationReaders implements LocationReader {
for (const item of items) {
if (item.type === 'location') {
await this.handleLocation(item, emit);
} else if (item.type === 'data') {
await this.handleData(item, emit);
} else if (item.type === 'entity') {
if (rulesEnforcer.isAllowed(item.entity, item.location)) {
const relations = Array<EntityRelationSpec>();
@@ -165,40 +162,6 @@ export class LocationReaders implements LocationReader {
logger.warn(message);
}
private async handleData(
item: CatalogProcessorDataResult,
emit: CatalogProcessorEmit,
) {
const { processors, logger } = this.options;
const validatedEmit: CatalogProcessorEmit = emitResult => {
if (emitResult.type === 'relation') {
throw new Error('parseData may not emit entity relations');
}
emit(emitResult);
};
for (const processor of processors) {
if (processor.parseData) {
try {
if (
await processor.parseData(item.data, item.location, validatedEmit)
) {
return;
}
} catch (e) {
const message = `Processor ${processor.constructor.name} threw an error while parsing ${item.location.type} ${item.location.target}, ${e}`;
emit(result.generalError(item.location, message));
logger.warn(message);
}
}
}
const message = `No processor was able to parse location ${item.location.type} ${item.location.target}`;
emit(result.inputError(item.location, message));
}
private async handleEntity(
item: CatalogProcessorEntityResult,
emit: CatalogProcessorEmit,
@@ -1,146 +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 { LocationSpec } from '@backstage/catalog-model';
import { Config } from '@backstage/config';
import fetch from 'cross-fetch';
import * as result from './results';
import { CatalogProcessor, CatalogProcessorEmit } from './types';
// ***********************************************************************
// * NOTE: This has been replaced by packages/backend-common/src/reading *
// * Don't implement new functionality here as this file will be removed *
// ***********************************************************************
export class AzureApiReaderProcessor implements CatalogProcessor {
private privateToken: string;
constructor(config: Config) {
this.privateToken =
config.getOptionalString('catalog.processors.azureApi.privateToken') ??
'';
}
getRequestOptions(): RequestInit {
const headers: HeadersInit = {};
if (this.privateToken !== '') {
headers.Authorization = `Basic ${Buffer.from(
`:${this.privateToken}`,
'utf8',
).toString('base64')}`;
}
return {
headers,
};
}
async readLocation(
location: LocationSpec,
optional: boolean,
emit: CatalogProcessorEmit,
): Promise<boolean> {
if (location.type !== 'azure/api') {
return false;
}
try {
const url = this.buildRawUrl(location.target);
const response = await fetch(url.toString(), this.getRequestOptions());
// for private repos when PAT is not valid, Azure API returns a http status code 203 with sign in page html
if (response.ok && response.status !== 203) {
const data = Buffer.from(await response.text());
emit(result.data(location, data));
} else {
const message = `${location.target} could not be read as ${url}, ${response.status} ${response.statusText}`;
if (response.status === 404) {
if (!optional) {
emit(result.notFoundError(location, message));
}
} else {
emit(result.generalError(location, message));
}
}
} catch (e) {
const message = `Unable to read ${location.type} ${location.target}, ${e}`;
emit(result.generalError(location, message));
}
return true;
}
// Converts
// from: https://dev.azure.com/{organization}/{project}/_git/reponame?path={path}&version=GB{commitOrBranch}&_a=contents
// to: https://dev.azure.com/{organization}/{project}/_apis/git/repositories/reponame/items?path={path}&version={commitOrBranch}
buildRawUrl(target: string): URL {
try {
const url = new URL(target);
const [
empty,
userOrOrg,
project,
srcKeyword,
repoName,
] = url.pathname.split('/');
const path = url.searchParams.get('path') || '';
const ref = url.searchParams.get('version')?.substr(2);
if (
url.hostname !== 'dev.azure.com' ||
empty !== '' ||
userOrOrg === '' ||
project === '' ||
srcKeyword !== '_git' ||
repoName === '' ||
path === '' ||
ref === ''
) {
throw new Error('Wrong Azure Devops URL or Invalid file path');
}
// transform to api
url.pathname = [
empty,
userOrOrg,
project,
'_apis',
'git',
'repositories',
repoName,
'items',
].join('/');
const queryParams = [`path=${path}`];
if (ref) {
queryParams.push(`version=${ref}`);
}
url.search = queryParams.join('&');
url.protocol = 'https';
return url;
} catch (e) {
throw new Error(`Incorrect url: ${target}, ${e}`);
}
}
}
@@ -1,138 +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 { LocationSpec } from '@backstage/catalog-model';
import fetch from 'cross-fetch';
import * as result from './results';
import { Config } from '@backstage/config';
import { CatalogProcessor, CatalogProcessorEmit } from './types';
// ***********************************************************************
// * NOTE: This has been replaced by packages/backend-common/src/reading *
// * Don't implement new functionality here as this file will be removed *
// ***********************************************************************
export class BitbucketApiReaderProcessor implements CatalogProcessor {
private username: string;
private password: string;
constructor(config: Config) {
this.username =
config.getOptionalString('catalog.processors.bitbucketApi.username') ??
'';
this.password =
config.getOptionalString('catalog.processors.bitbucketApi.appPassword') ??
'';
}
getRequestOptions(): RequestInit {
const headers: HeadersInit = {};
if (this.username !== '' && this.password !== '') {
headers.Authorization = `Basic ${Buffer.from(
`${this.username}:${this.password}`,
'utf8',
).toString('base64')}`;
}
const requestOptions: RequestInit = {
headers,
};
return requestOptions;
}
async readLocation(
location: LocationSpec,
optional: boolean,
emit: CatalogProcessorEmit,
): Promise<boolean> {
if (location.type !== 'bitbucket/api') {
return false;
}
try {
const url = this.buildRawUrl(location.target);
const response = await fetch(url.toString(), this.getRequestOptions());
if (response.ok) {
const data = await response.text();
emit(result.data(location, Buffer.from(data)));
} else {
const message = `${location.target} could not be read as ${url}, ${response.status} ${response.statusText}`;
if (response.status === 404) {
if (!optional) {
emit(result.notFoundError(location, message));
}
} else {
emit(result.generalError(location, message));
}
}
} catch (e) {
const message = `Unable to read ${location.type} ${location.target}, ${e}`;
emit(result.generalError(location, message));
}
return true;
}
// Converts
// from: https://bitbucket.org/orgname/reponame/src/master/file.yaml
// to: https://api.bitbucket.org/2.0/repositories/orgname/reponame/src/master/file.yaml
buildRawUrl(target: string): URL {
try {
const url = new URL(target);
const [
empty,
userOrOrg,
repoName,
srcKeyword,
ref,
...restOfPath
] = url.pathname.split('/');
if (
url.hostname !== 'bitbucket.org' ||
empty !== '' ||
userOrOrg === '' ||
repoName === '' ||
srcKeyword !== 'src'
) {
throw new Error('Wrong Bitbucket URL or Invalid file path');
}
// transform to api
url.pathname = [
empty,
'2.0',
'repositories',
userOrOrg,
repoName,
'src',
ref,
...restOfPath,
].join('/');
url.hostname = 'api.bitbucket.org';
url.protocol = 'https';
return url;
} catch (e) {
throw new Error(`Incorrect url: ${target}, ${e}`);
}
}
}
@@ -18,6 +18,7 @@ import { LocationSpec } from '@backstage/catalog-model';
import fs from 'fs-extra';
import * as result from './results';
import { CatalogProcessor, CatalogProcessorEmit } from './types';
import { parseEntityYaml } from './util/parse';
export class FileReaderProcessor implements CatalogProcessor {
async readLocation(
@@ -33,7 +34,10 @@ export class FileReaderProcessor implements CatalogProcessor {
const exists = await fs.pathExists(location.target);
if (exists) {
const data = await fs.readFile(location.target);
emit(result.data(location, data));
for (const parseResult of parseEntityYaml(data, location)) {
emit(parseResult);
}
} else if (!optional) {
const message = `${location.type} ${location.target} does not exist`;
emit(result.notFoundError(location, message));
@@ -1,274 +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 { LocationSpec } from '@backstage/catalog-model';
import { Config } from '@backstage/config';
import parseGitUri from 'git-url-parse';
import fetch from 'cross-fetch';
import { Logger } from 'winston';
import * as result from './results';
import { CatalogProcessor, CatalogProcessorEmit } from './types';
// ***********************************************************************
// * NOTE: This has been replaced by packages/backend-common/src/reading *
// * Don't implement new functionality here as this file will be removed *
// ***********************************************************************
/**
* The configuration parameters for a single GitHub API provider.
*/
export type ProviderConfig = {
/**
* The prefix of the target that this matches on, e.g. "https://github.com",
* with no trailing slash.
*/
target: string;
/**
* The base URL of the API of this provider, e.g. "https://api.github.com",
* with no trailing slash.
*
* May be omitted specifically for GitHub; then it will be deduced.
*
* The API will always be preferred if both its base URL and a token are
* present.
*/
apiBaseUrl?: string;
/**
* The base URL of the raw fetch endpoint of this provider, e.g.
* "https://raw.githubusercontent.com", with no trailing slash.
*
* May be omitted specifically for GitHub; then it will be deduced.
*
* The API will always be preferred if both its base URL and a token are
* present.
*/
rawBaseUrl?: string;
/**
* The authorization token to use for requests to this provider.
*
* If no token is specified, anonymous access is used.
*/
token?: string;
};
export function getApiRequestOptions(provider: ProviderConfig): RequestInit {
const headers: HeadersInit = {
Accept: 'application/vnd.github.v3.raw',
};
if (provider.token) {
headers.Authorization = `token ${provider.token}`;
}
return {
headers,
};
}
export function getRawRequestOptions(provider: ProviderConfig): RequestInit {
const headers: HeadersInit = {};
if (provider.token) {
headers.Authorization = `token ${provider.token}`;
}
return {
headers,
};
}
// Converts for example
// from: https://github.com/a/b/blob/branchname/path/to/c.yaml
// to: https://api.github.com/repos/a/b/contents/path/to/c.yaml?ref=branchname
export function getApiUrl(target: string, provider: ProviderConfig): URL {
try {
const { owner, name, ref, filepathtype, filepath } = parseGitUri(target);
if (
!owner ||
!name ||
!ref ||
(filepathtype !== 'blob' && filepathtype !== 'raw')
) {
throw new Error('Wrong URL or invalid file path');
}
const pathWithoutSlash = filepath.replace(/^\//, '');
return new URL(
`${provider.apiBaseUrl}/repos/${owner}/${name}/contents/${pathWithoutSlash}?ref=${ref}`,
);
} catch (e) {
throw new Error(`Incorrect URL: ${target}, ${e}`);
}
}
// Converts for example
// from: https://github.com/a/b/blob/branchname/c.yaml
// to: https://raw.githubusercontent.com/a/b/branchname/c.yaml
export function getRawUrl(target: string, provider: ProviderConfig): URL {
try {
const { owner, name, ref, filepathtype, filepath } = parseGitUri(target);
if (
!owner ||
!name ||
!ref ||
(filepathtype !== 'blob' && filepathtype !== 'raw')
) {
throw new Error('Wrong URL or invalid file path');
}
const pathWithoutSlash = filepath.replace(/^\//, '');
return new URL(
`${provider.rawBaseUrl}/${owner}/${name}/${ref}/${pathWithoutSlash}`,
);
} catch (e) {
throw new Error(`Incorrect URL: ${target}, ${e}`);
}
}
export function readConfig(config: Config, logger: Logger): ProviderConfig[] {
const providers: ProviderConfig[] = [];
// TODO(freben): Deprecate the old config root entirely in a later release
if (config.has('catalog.processors.githubApi')) {
logger.warn(
'The catalog.processors.githubApi configuration key has been deprecated, please use catalog.processors.github instead',
);
}
// In a previous version of the configuration, we only supported github,
// and the "privateToken" key held the token to use for it. The new
// configuration method is to use the "providers" key instead.
const providerConfigs =
config.getOptionalConfigArray('catalog.processors.github.providers') ??
config.getOptionalConfigArray('catalog.processors.githubApi.providers') ??
[];
const legacyToken =
config.getOptionalString('catalog.processors.github.privateToken') ??
config.getOptionalString('catalog.processors.githubApi.privateToken');
// First read all the explicit providers
for (const providerConfig of providerConfigs) {
const target = providerConfig.getString('target').replace(/\/+$/, '');
let apiBaseUrl = providerConfig.getOptionalString('apiBaseUrl');
let rawBaseUrl = providerConfig.getOptionalString('rawBaseUrl');
const token = providerConfig.getOptionalString('token');
if (apiBaseUrl) {
apiBaseUrl = apiBaseUrl.replace(/\/+$/, '');
} else if (target === 'https://github.com') {
apiBaseUrl = 'https://api.github.com';
}
if (rawBaseUrl) {
rawBaseUrl = rawBaseUrl.replace(/\/+$/, '');
} else if (target === 'https://github.com') {
rawBaseUrl = 'https://raw.githubusercontent.com';
}
if (!apiBaseUrl && !rawBaseUrl) {
throw new Error(
`Provider at ${target} must configure an explicit apiBaseUrl or rawBaseUrl`,
);
}
providers.push({ target, apiBaseUrl, rawBaseUrl, token });
}
// If no explicit github.com provider was added, put one in the list as
// a convenience
if (!providers.some(p => p.target === 'https://github.com')) {
providers.push({
target: 'https://github.com',
apiBaseUrl: 'https://api.github.com',
rawBaseUrl: 'https://raw.githubusercontent.com',
token: legacyToken,
});
}
return providers;
}
/**
* A processor that adds the ability to read files from GitHub v3 APIs, such as
* the one exposed by GitHub itself.
*/
export class GithubReaderProcessor implements CatalogProcessor {
private providers: ProviderConfig[];
static fromConfig(config: Config, logger: Logger) {
return new GithubReaderProcessor(readConfig(config, logger));
}
constructor(providers: ProviderConfig[]) {
this.providers = providers;
}
async readLocation(
location: LocationSpec,
optional: boolean,
emit: CatalogProcessorEmit,
): Promise<boolean> {
// The github/api type is for backward compatibility
if (location.type !== 'github' && location.type !== 'github/api') {
return false;
}
const provider = this.providers.find(p =>
location.target.startsWith(`${p.target}/`),
);
if (!provider) {
throw new Error(
`There is no GitHub provider that matches ${location.target}. Please add a configuration entry for it under catalog.processors.github.providers.`,
);
}
try {
const useApi =
provider.apiBaseUrl && (provider.token || !provider.rawBaseUrl);
const url = useApi
? getApiUrl(location.target, provider)
: getRawUrl(location.target, provider);
const options = useApi
? getApiRequestOptions(provider)
: getRawRequestOptions(provider);
const response = await fetch(url.toString(), options);
if (response.ok) {
const data = await response.text();
emit(result.data(location, Buffer.from(data)));
} else {
const message = `${location.target} could not be read as ${url}, ${response.status} ${response.statusText}`;
if (response.status === 404) {
if (!optional) {
emit(result.notFoundError(location, message));
}
} else {
emit(result.generalError(location, message));
}
}
} catch (e) {
const message = `Unable to read ${location.type} ${location.target}, ${e}`;
emit(result.generalError(location, message));
}
return true;
}
}
@@ -1,141 +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 { LocationSpec } from '@backstage/catalog-model';
import fetch from 'cross-fetch';
import { Config } from '@backstage/config';
import * as result from './results';
import { CatalogProcessor, CatalogProcessorEmit } from './types';
// ***********************************************************************
// * NOTE: This has been replaced by packages/backend-common/src/reading *
// * Don't implement new functionality here as this file will be removed *
// ***********************************************************************
export class GitlabApiReaderProcessor implements CatalogProcessor {
private privateToken: string;
constructor(config: Config) {
this.privateToken =
config.getOptionalString('catalog.processors.gitlabApi.privateToken') ??
'';
}
getRequestOptions(): RequestInit {
const headers: HeadersInit = { 'PRIVATE-TOKEN': '' };
if (this.privateToken !== '') {
headers['PRIVATE-TOKEN'] = this.privateToken;
}
const requestOptions: RequestInit = {
headers,
};
return requestOptions;
}
async readLocation(
location: LocationSpec,
optional: boolean,
emit: CatalogProcessorEmit,
): Promise<boolean> {
if (location.type !== 'gitlab/api') {
return false;
}
try {
const projectID = await this.getProjectID(location.target);
const url = this.buildRawUrl(location.target, projectID);
const response = await fetch(url.toString(), this.getRequestOptions());
if (response.ok) {
const data = await response.text();
emit(result.data(location, Buffer.from(data)));
} else {
const message = `${location.target} could not be read as ${url}, ${response.status} ${response.statusText}`;
if (response.status === 404) {
if (!optional) {
emit(result.notFoundError(location, message));
}
} else {
emit(result.generalError(location, message));
}
}
} catch (e) {
const message = `Unable to read ${location.type} ${location.target}, ${e}`;
emit(result.generalError(location, message));
}
return true;
}
// convert https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/filepath
// to https://gitlab.com/api/v4/projects/<PROJECTID>/repository/files/filepath?ref=branch
buildRawUrl(target: string, projectID: Number): URL {
try {
const url = new URL(target);
const branchAndfilePath = url.pathname.split('/-/blob/')[1];
const [branch, ...filePath] = branchAndfilePath.split('/');
url.pathname = [
'/api/v4/projects',
projectID,
'repository/files',
encodeURIComponent(filePath.join('/')),
'raw',
].join('/');
url.search = `?ref=${branch}`;
return url;
} catch (e) {
throw new Error(`Incorrect url: ${target}, ${e}`);
}
}
async getProjectID(target: string): Promise<Number> {
const url = new URL(target);
if (
// absPaths to gitlab files should contain /-/blob
// ex: https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/filepath
!url.pathname.match(/\/\-\/blob\//)
) {
throw new Error('Please provide full path to yaml file from Gitlab');
}
try {
const repo = url.pathname.split('/-/blob/')[0];
// Find ProjectID from url
// convert 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/filepath'
// to 'https://gitlab.com/api/v4/projects/groupA%2Fteams%2FsubgroupA%2FteamA%2Frepo'
const repoIDLookup = new URL(
`${url.protocol + url.hostname}/api/v4/projects/${encodeURIComponent(
repo.replace(/^\//, ''),
)}`,
);
const response = await fetch(
repoIDLookup.toString(),
this.getRequestOptions(),
);
const projectIDJson = await response.json();
const projectID: Number = projectIDJson.id;
return projectID;
} catch (e) {
throw new Error(`Could not get GitLab ProjectID for: ${target}, ${e}`);
}
}
}
@@ -1,94 +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 { LocationSpec } from '@backstage/catalog-model';
import fetch from 'cross-fetch';
import * as result from './results';
import { CatalogProcessor, CatalogProcessorEmit } from './types';
// ***********************************************************************
// * NOTE: This has been replaced by packages/backend-common/src/reading *
// * Don't implement new functionality here as this file will be removed *
// ***********************************************************************
export class GitlabReaderProcessor implements CatalogProcessor {
async readLocation(
location: LocationSpec,
optional: boolean,
emit: CatalogProcessorEmit,
): Promise<boolean> {
if (location.type !== 'gitlab') {
return false;
}
try {
const url = this.buildRawUrl(location.target);
const response = await fetch(url.toString());
if (response.ok) {
const data = await response.text();
emit(result.data(location, Buffer.from(data)));
} else {
const message = `${location.target} could not be read as ${url}, ${response.status} ${response.statusText}`;
if (response.status === 404) {
if (!optional) {
throw result.notFoundError(location, message);
}
} else {
throw result.generalError(location, message);
}
}
} catch (e) {
const message = `Unable to read ${location.type} ${location.target}, ${e}`;
emit(result.generalError(location, message));
}
return true;
}
// Converts
// from: https://gitlab.example.com/a/b/blob/master/c.yaml
// to: https://gitlab.example.com/a/b/raw/master/c.yaml
private buildRawUrl(target: string): URL {
try {
const url = new URL(target);
const [empty, userOrOrg, repoName, , ...restOfPath] = url.pathname
.split('/')
// for the common case https://gitlab.example.com/a/b/-/blob/master/c.yaml
.filter(path => path !== '-');
if (
empty !== '' ||
userOrOrg === '' ||
repoName === '' ||
!restOfPath.join('/').match(/\.yaml$/)
) {
throw new Error('Wrong GitLab URL');
}
// Replace 'blob' with 'raw'
url.pathname = [empty, userOrOrg, repoName, 'raw', ...restOfPath].join(
'/',
);
return url;
} catch (e) {
throw new Error(`Incorrect url: ${target}, ${e}`);
}
}
}
@@ -21,7 +21,7 @@ import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { msw } from '@backstage/test-utils';
import {
CatalogProcessorDataResult,
CatalogProcessorEntityResult,
CatalogProcessorErrorResult,
CatalogProcessorResult,
} from './types';
@@ -42,17 +42,17 @@ describe('UrlReaderProcessor', () => {
server.use(
rest.get(`${mockApiOrigin}/component.yaml`, (_, res, ctx) =>
res(ctx.body('Hello')),
res(ctx.json({ mock: 'entity' })),
),
);
const generated = (await new Promise<CatalogProcessorResult>(emit =>
processor.readLocation(spec, false, emit),
)) as CatalogProcessorDataResult;
)) as CatalogProcessorEntityResult;
expect(generated.type).toBe('data');
expect(generated.type).toBe('entity');
expect(generated.location).toBe(spec);
expect(generated.data.toString('utf8')).toBe('Hello');
expect(generated.entity).toEqual({ mock: 'entity' });
});
it('should fail load from url with error', async () => {
@@ -19,6 +19,7 @@ import { LocationSpec } from '@backstage/catalog-model';
import { Logger } from 'winston';
import * as result from './results';
import { CatalogProcessor, CatalogProcessorEmit } from './types';
import { parseEntityYaml } from './util/parse';
// TODO(Rugvip): Added for backwards compatibility when moving to UrlReader, this
// can be removed in a bit
@@ -55,7 +56,10 @@ export class UrlReaderProcessor implements CatalogProcessor {
try {
const data = await this.options.reader.read(location.target);
emit(result.data(location, data));
for (const parseResult of parseEntityYaml(data, location)) {
emit(parseResult);
}
} catch (error) {
const message = `Unable to read ${location.type}, ${error}`;
@@ -1,151 +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 } from '@backstage/catalog-model';
import { TextEncoder } from 'util';
import yaml from 'yaml';
import {
CatalogProcessorEntityResult,
CatalogProcessorErrorResult,
} from './types';
import { YamlProcessor } from './YamlProcessor';
describe('YamlProcessor', () => {
const processor = new YamlProcessor();
const locationSpec = {
type: 'url',
target: 'http://example.com/component.yaml',
};
function encodeEntity(entity: string): Buffer {
const data = new TextEncoder().encode(entity);
return Buffer.from(data);
}
it('should only process files with yaml', async () => {
const wrongLocationSpec = {
type: 'url',
target: 'http://example.com/component.json',
};
const buffer = Buffer.from([]);
const never = jest.fn();
expect(await processor.parseData(buffer, wrongLocationSpec, never)).toBe(
false,
);
expect(never).not.toBeCalled();
});
it('should process url that contains yaml', async () => {
const containsYamlLocationSpec = {
type: 'url',
target: 'http://example.com/component?path=test.yaml&c=1&d=2',
};
const buffer = Buffer.from([]);
const emit = jest.fn();
expect(
await processor.parseData(buffer, containsYamlLocationSpec, emit),
).toBe(true);
expect(emit).toBeCalled();
});
it('should process entity with yaml', async () => {
const entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
name: 'my-component',
},
spec: {},
} as Entity;
const buffer = encodeEntity(yaml.stringify(entity));
const emit = jest.fn();
expect(await processor.parseData(buffer, locationSpec, emit)).toBe(true);
const e = emit.mock.calls[0][0] as CatalogProcessorEntityResult;
expect(e.type).toBe('entity');
expect(e.location).toBe(locationSpec);
expect(e.entity).toEqual(entity);
});
it('should process multiple entities with yaml', async () => {
const entityComponent = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
name: 'my-component',
},
spec: {},
} as Entity;
const entityApi = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'API',
metadata: {
name: 'my-api',
},
spec: {},
} as Entity;
const buffer = encodeEntity(
`${yaml.stringify(entityComponent)}---\n${yaml.stringify(entityApi)}`,
);
const emit = jest.fn();
expect(await processor.parseData(buffer, locationSpec, emit)).toBe(true);
const eComponent = emit.mock.calls[0][0] as CatalogProcessorEntityResult;
expect(eComponent.type).toBe('entity');
expect(eComponent.location).toBe(locationSpec);
expect(eComponent.entity).toEqual(entityComponent);
const eApi = emit.mock.calls[1][0] as CatalogProcessorEntityResult;
expect(eApi.type).toBe('entity');
expect(eApi.location).toBe(locationSpec);
expect(eApi.entity).toEqual(entityApi);
});
it('should fail process entity on invalid yaml', async () => {
const buffer = encodeEntity('{');
const emit = jest.fn();
expect(await processor.parseData(buffer, locationSpec, emit)).toBe(true);
const e = emit.mock.calls[0][0] as CatalogProcessorErrorResult;
expect(e.error.message).toMatch(/^YAML error, /);
expect(e.type).toBe('error');
expect(e.location).toBe(locationSpec);
});
it('should fail process entity if not object at root', async () => {
const buffer = encodeEntity('[]');
const emit = jest.fn();
expect(await processor.parseData(buffer, locationSpec, emit)).toBe(true);
const e = emit.mock.calls[0][0] as CatalogProcessorErrorResult;
expect(e.error.message).toMatch(/^Expected object at root, got /);
expect(e.type).toBe('error');
expect(e.location).toBe(locationSpec);
});
});
@@ -1,63 +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, LocationSpec } from '@backstage/catalog-model';
import lodash from 'lodash';
import yaml from 'yaml';
import * as result from './results';
import { CatalogProcessor, CatalogProcessorEmit } from './types';
/**
* Handles incoming raw data buffers, and if they have a yaml extension,
* attempts to parse them into structured data and emitting them as un-
* validated entities.
*/
export class YamlProcessor implements CatalogProcessor {
async parseData(
data: Buffer,
location: LocationSpec,
emit: CatalogProcessorEmit,
): Promise<boolean> {
if (!location.target.match(/\.ya?ml/)) {
return false;
}
let documents: yaml.Document.Parsed[];
try {
documents = yaml.parseAllDocuments(data.toString('utf8')).filter(d => d);
} catch (e) {
emit(result.generalError(location, `Failed to parse YAML, ${e}`));
return true;
}
for (const document of documents) {
if (document.errors?.length) {
const message = `YAML error, ${document.errors[0]}`;
emit(result.generalError(location, message));
} else {
const json = document.toJSON();
if (lodash.isPlainObject(json)) {
emit(result.entity(location, json as Entity));
} else {
const message = `Expected object at root, got ${typeof json}`;
emit(result.generalError(location, message));
}
}
}
return true;
}
}
@@ -20,18 +20,12 @@ export { results };
export * from './types';
export { AnnotateLocationEntityProcessor } from './AnnotateLocationEntityProcessor';
export { AzureApiReaderProcessor } from './AzureApiReaderProcessor';
export { BitbucketApiReaderProcessor } from './BitbucketApiReaderProcessor';
export { CodeOwnersProcessor } from './CodeOwnersProcessor';
export { FileReaderProcessor } from './FileReaderProcessor';
export { GithubOrgReaderProcessor } from './GithubOrgReaderProcessor';
export { GithubReaderProcessor } from './GithubReaderProcessor';
export { GitlabApiReaderProcessor } from './GitlabApiReaderProcessor';
export { GitlabReaderProcessor } from './GitlabReaderProcessor';
export { OwnerRelationProcessor } from './OwnerRelationProcessor';
export { LocationRefProcessor } from './LocationEntityProcessor';
export { PlaceholderProcessor } from './PlaceholderProcessor';
export type { PlaceholderResolver } from './PlaceholderProcessor';
export { StaticLocationProcessor } from './StaticLocationProcessor';
export { UrlReaderProcessor } from './UrlReaderProcessor';
export { YamlProcessor } from './YamlProcessor';
@@ -51,13 +51,6 @@ export function generalError(
return { type: 'error', location: atLocation, error: new Error(message) };
}
export function data(
atLocation: LocationSpec,
newData: Buffer,
): CatalogProcessorResult {
return { type: 'data', location: atLocation, data: newData };
}
export function location(
newLocation: LocationSpec,
optional: boolean,
@@ -35,20 +35,6 @@ export type CatalogProcessor = {
emit: CatalogProcessorEmit,
): Promise<boolean>;
/**
* Parses a raw data buffer that was read from a location.
*
* @param data The data to parse
* @param location The location that the data came from
* @param emit A sink for items resulting from the parsing
* @returns True if handled by this processor, false otherwise
*/
parseData?(
data: Buffer,
location: LocationSpec,
emit: CatalogProcessorEmit,
): Promise<boolean>;
/**
* Pre-processes an emitted entity, after it has been emitted but before it
* has been validated.
@@ -105,12 +91,6 @@ export type CatalogProcessorLocationResult = {
optional: boolean;
};
export type CatalogProcessorDataResult = {
type: 'data';
data: Buffer;
location: LocationSpec;
};
export type CatalogProcessorEntityResult = {
type: 'entity';
entity: Entity;
@@ -131,7 +111,6 @@ export type CatalogProcessorErrorResult = {
export type CatalogProcessorResult =
| CatalogProcessorLocationResult
| CatalogProcessorDataResult
| CatalogProcessorEntityResult
| CatalogProcessorRelationResult
| CatalogProcessorErrorResult;
@@ -0,0 +1,180 @@
/*
* 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 { parseEntityYaml } from './parse';
import * as result from '../results';
const testLoc = {
target: 'my-loc-target',
type: 'my-loc-type',
};
describe('parseEntityYaml', () => {
it('should parse a yaml', () => {
const results = Array.from(
parseEntityYaml(
Buffer.from(
`
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage
description: backstage.io
annotations:
github.com/project-slug: 'spotify/backstage'
spec:
type: website
lifecycle: production
owner: guest
`,
'utf8',
),
testLoc,
),
);
expect(results).toEqual([
result.entity(testLoc, {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
name: 'backstage',
description: 'backstage.io',
annotations: {
'github.com/project-slug': 'spotify/backstage',
},
},
spec: {
type: 'website',
lifecycle: 'production',
owner: 'guest',
},
}),
]);
});
it('should parse multiple docs', () => {
const results = Array.from(
parseEntityYaml(
Buffer.from(
`
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: web
spec:
type: website
---
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: srv
spec:
type: service
`,
'utf8',
),
testLoc,
),
);
expect(results).toEqual([
result.entity(testLoc, {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
name: 'web',
},
spec: {
type: 'website',
},
}),
result.entity(testLoc, {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
name: 'srv',
},
spec: {
type: 'service',
},
}),
]);
});
it('should emit parsing errors', () => {
const results = Array.from(
parseEntityYaml(Buffer.from('`', 'utf8'), testLoc),
);
// Parse errors are always per document
expect(results).toEqual([
result.generalError(
testLoc,
'YAML error, YAMLSemanticError: Plain value cannot start with reserved character `',
),
]);
});
it('should emit parsing errors for individual documents', () => {
const results = Array.from(
parseEntityYaml(
Buffer.from(
`
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: web
spec:
type: website
---
apiVersion: backstage.io/v1alpha1
this: - is - not [valid] yaml
`,
'utf8',
),
testLoc,
),
);
expect(results).toEqual([
result.entity(testLoc, {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
name: 'web',
},
spec: {
type: 'website',
},
}),
result.generalError(
testLoc,
'YAML error, YAMLSemanticError: Nested mappings are not allowed in compact mappings',
),
]);
});
it('must be an object at root', () => {
const results = Array.from(
parseEntityYaml(Buffer.from('imma-string', 'utf8'), testLoc),
);
expect(results).toEqual([
result.generalError(testLoc, 'Expected object at root, got string'),
]);
});
});
@@ -0,0 +1,49 @@
/*
* 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, LocationSpec } from '@backstage/catalog-model';
import lodash from 'lodash';
import yaml from 'yaml';
import * as result from '../results';
import { CatalogProcessorResult } from '../types';
export function* parseEntityYaml(
data: Buffer,
location: LocationSpec,
): Iterable<CatalogProcessorResult> {
let documents: yaml.Document.Parsed[];
try {
documents = yaml.parseAllDocuments(data.toString('utf8')).filter(d => d);
} catch (e) {
yield result.generalError(location, `Failed to parse YAML, ${e}`);
return;
}
for (const document of documents) {
if (document.errors?.length) {
const message = `YAML error, ${document.errors[0]}`;
yield result.generalError(location, message);
} else {
const json = document.toJSON();
if (lodash.isPlainObject(json)) {
yield result.entity(location, json as Entity);
} else {
const message = `Expected object at root, got ${typeof json}`;
yield result.generalError(location, message);
}
}
}
}
@@ -15,24 +15,43 @@
*/
import { getVoidLogger, UrlReader } from '@backstage/backend-common';
import { Entity, LocationSpec } from '@backstage/catalog-model';
import { Entity } from '@backstage/catalog-model';
import { ConfigReader } from '@backstage/config';
import Knex from 'knex';
import yaml from 'yaml';
import { DatabaseManager } from '../database';
import { CatalogProcessorEmit } from '../ingestion';
import * as result from '../ingestion/processors/results';
import { CatalogBuilder, CatalogEnvironment } from './CatalogBuilder';
const dummyEntity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
name: 'n',
},
spec: {
type: 't',
owner: 'o',
lifecycle: 'l',
},
};
const dummyEntityYaml = yaml.stringify(dummyEntity);
describe('CatalogBuilder', () => {
const db = DatabaseManager.createTestDatabaseConnection();
let db: Knex<any, unknown[]>;
const reader: jest.Mocked<UrlReader> = { read: jest.fn() };
const env: CatalogEnvironment = {
logger: getVoidLogger(),
database: { getClient: () => db },
database: { getClient: async () => db },
config: ConfigReader.fromConfigs([]),
reader,
};
afterEach(() => jest.resetAllMocks());
beforeEach(async () => {
db = await DatabaseManager.createTestDatabaseConnection();
jest.resetAllMocks();
});
it('works with no changes', async () => {
const builder = new CatalogBuilder(env);
@@ -46,7 +65,7 @@ describe('CatalogBuilder', () => {
});
it('works with everything replaced', async () => {
reader.read.mockResolvedValue(Buffer.from('junk'));
reader.read.mockResolvedValueOnce(Buffer.from('junk'));
const builder = new CatalogBuilder(env)
.replaceEntityPolicies([
@@ -77,21 +96,8 @@ describe('CatalogBuilder', () => {
})
.replaceProcessors([
{
async readLocation(
location: LocationSpec,
_optional: boolean,
emit: CatalogProcessorEmit,
) {
async readLocation(location, _optional, emit) {
expect(location.type).toBe('test');
emit(result.data(location, await reader.read('ignored')));
return true;
},
async parseData(
data: Buffer,
location: LocationSpec,
emit: CatalogProcessorEmit,
) {
expect(data.toString()).toEqual('junk');
emit(
result.entity(location, {
apiVersion: 'av',
@@ -101,14 +107,14 @@ describe('CatalogBuilder', () => {
);
return true;
},
async preProcessEntity(entity: Entity) {
async preProcessEntity(entity) {
expect(entity.apiVersion).toBe('av');
return {
...entity,
metadata: { ...entity.metadata, namespace: 'ns' },
};
},
async postProcessEntity(entity: Entity) {
async postProcessEntity(entity) {
expect(entity.metadata.namespace).toBe('ns');
return {
...entity,
@@ -123,7 +129,7 @@ describe('CatalogBuilder', () => {
type: 'test',
target: '',
});
expect.assertions(8);
expect.assertions(7);
expect(added.entities).toEqual([
{
apiVersion: 'av',
@@ -141,4 +147,65 @@ describe('CatalogBuilder', () => {
},
]);
});
it('addProcessor works', async () => {
reader.read.mockResolvedValueOnce(Buffer.from(dummyEntityYaml));
const builder = new CatalogBuilder(env);
builder.addProcessor({
async preProcessEntity(e) {
return { ...e, metadata: { ...e.metadata, foo: 7 } };
},
});
const { entitiesCatalog, higherOrderOperation } = await builder.build();
await higherOrderOperation.addLocation({
type: 'github',
target: 'https://github.com/a/b/x.yaml',
});
const entities = await entitiesCatalog.entities();
expect(entities).toEqual([
expect.objectContaining({
metadata: expect.objectContaining({
foo: 7,
}),
}),
]);
});
it('replaceProcessors works', async () => {
reader.read.mockResolvedValueOnce(Buffer.from(dummyEntityYaml));
const builder = new CatalogBuilder(env);
builder.replaceProcessors([
{
async readLocation(location, _optional, emit) {
expect(location.type).toBe('x');
emit(result.entity(location, dummyEntity));
return true;
},
async preProcessEntity(e) {
expect(e.metadata.name).toBe('n');
return { ...e, metadata: { ...e.metadata, foo: 7 } };
},
},
]);
const { entitiesCatalog, higherOrderOperation } = await builder.build();
await higherOrderOperation.addLocation({
type: 'x',
target: 'y',
});
const entities = await entitiesCatalog.entities();
expect.assertions(3);
expect(entities).toEqual([
expect.objectContaining({
metadata: expect.objectContaining({
foo: 7,
}),
}),
]);
});
});
@@ -43,15 +43,10 @@ import {
import { DatabaseManager } from '../database';
import {
AnnotateLocationEntityProcessor,
AzureApiReaderProcessor,
BitbucketApiReaderProcessor,
CatalogProcessor,
CodeOwnersProcessor,
FileReaderProcessor,
GithubOrgReaderProcessor,
GithubReaderProcessor,
GitlabApiReaderProcessor,
GitlabReaderProcessor,
OwnerRelationProcessor,
HigherOrderOperation,
HigherOrderOperations,
@@ -61,7 +56,6 @@ import {
PlaceholderResolver,
StaticLocationProcessor,
UrlReaderProcessor,
YamlProcessor,
} from '../ingestion';
import { CatalogRulesEnforcer } from '../ingestion/CatalogRules';
import { LdapOrgReaderProcessor } from '../ingestion/processors/LdapOrgReaderProcessor';
@@ -318,6 +312,8 @@ export class CatalogBuilder {
private buildProcessors(): CatalogProcessor[] {
const { config, logger, reader } = this.env;
this.checkDeprecatedReaderProcessors();
const placeholderResolvers: Record<string, PlaceholderResolver> = {
json: jsonPlaceholderResolver,
yaml: yamlPlaceholderResolver,
@@ -325,65 +321,55 @@ export class CatalogBuilder {
...this.placeholderResolvers,
};
const processors = this.processorsReplace
? this.processors
: [
new FileReaderProcessor(),
GithubOrgReaderProcessor.fromConfig(config, { logger }),
LdapOrgReaderProcessor.fromConfig(config, { logger }),
new UrlReaderProcessor({ reader, logger }),
new YamlProcessor(),
new CodeOwnersProcessor({ reader }),
new LocationRefProcessor(),
new OwnerRelationProcessor(),
new AnnotateLocationEntityProcessor(),
];
return [
// These are always there no matter what
const processors: CatalogProcessor[] = [
StaticLocationProcessor.fromConfig(config),
new PlaceholderProcessor({ resolvers: placeholderResolvers, reader }),
...this.buildDeprecatedReaderProcessors(),
...processors,
];
// These are only added unless the user replaced them all
if (!this.processorsReplace) {
processors.push(
new FileReaderProcessor(),
GithubOrgReaderProcessor.fromConfig(config, { logger }),
LdapOrgReaderProcessor.fromConfig(config, { logger }),
new UrlReaderProcessor({ reader, logger }),
new CodeOwnersProcessor({ reader }),
new LocationRefProcessor(),
new OwnerRelationProcessor(),
new AnnotateLocationEntityProcessor(),
);
}
// Add the ones (if any) that the user added
processors.push(...this.processors);
return processors;
}
// TODO(Rugvip): These are added for backwards compatibility if config exists
// The idea is to have everyone migrate from using the old processors to
// the new integration config driven UrlReaders. In an upcoming release we
// can then completely remove support for the old processors, but still
// keep handling the deprecated location types for a while, but with a
// warning.
private buildDeprecatedReaderProcessors(): CatalogProcessor[] {
const { config, logger } = this.env;
const result = [];
const pc = config.getOptionalConfig('catalog.processors');
// TODO(Rugvip): These old processors are removed, for a while we'll be throwing
// errors here to make sure people know where to move the config
private checkDeprecatedReaderProcessors() {
const pc = this.env.config.getOptionalConfig('catalog.processors');
if (pc?.has('github')) {
logger.warn(
throw new Error(
`Using deprecated configuration for catalog.processors.github, move to using integrations.github instead`,
);
result.push(GithubReaderProcessor.fromConfig(config, logger));
}
if (pc?.has('gitlabApi')) {
logger.warn(
throw new Error(
`Using deprecated configuration for catalog.processors.gitlabApi, move to using integrations.gitlab instead`,
);
result.push(new GitlabApiReaderProcessor(config));
result.push(new GitlabReaderProcessor());
}
if (pc?.has('bitbucketApi')) {
logger.warn(
throw new Error(
`Using deprecated configuration for catalog.processors.bitbucketApi, move to using integrations.bitbucket instead`,
);
result.push(new BitbucketApiReaderProcessor(config));
}
if (pc?.has('azureApi')) {
logger.warn(
throw new Error(
`Using deprecated configuration for catalog.processors.azureApi, move to using integrations.azure instead`,
);
result.push(new AzureApiReaderProcessor(config));
}
return result;
}
}
+8 -8
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-catalog-graphql",
"version": "0.1.1-alpha.25",
"version": "0.1.1-alpha.26",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -20,28 +20,28 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/backend-common": "^0.1.1-alpha.25",
"@backstage/catalog-model": "^0.1.1-alpha.25",
"@backstage/config": "^0.1.1-alpha.25",
"@backstage/backend-common": "^0.1.1-alpha.26",
"@backstage/catalog-model": "^0.1.1-alpha.26",
"@backstage/config": "^0.1.1-alpha.26",
"@graphql-modules/core": "^0.7.17",
"apollo-server": "^2.16.1",
"cross-fetch": "^3.0.6",
"graphql": "^15.3.0",
"graphql-tag": "^2.11.0",
"graphql-type-json": "^0.3.2",
"cross-fetch": "^3.0.6",
"winston": "^3.2.1"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.25",
"@backstage/test-utils": "^0.1.1-alpha.25",
"@backstage/cli": "^0.1.1-alpha.26",
"@backstage/test-utils": "^0.1.1-alpha.26",
"@graphql-codegen/cli": "^1.17.7",
"@graphql-codegen/typescript": "^1.17.7",
"@graphql-codegen/typescript-resolvers": "^1.17.7",
"@types/express": "^4.17.7",
"@types/supertest": "^2.0.8",
"eslint-plugin-graphql": "^4.0.0",
"supertest": "^4.0.2",
"msw": "^0.21.2",
"supertest": "^4.0.2",
"ts-node": "^8.10.2"
},
"files": [
+11 -11
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-catalog",
"version": "0.1.1-alpha.25",
"version": "0.1.1-alpha.26",
"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.25",
"@backstage/core": "^0.1.1-alpha.25",
"@backstage/plugin-scaffolder": "^0.1.1-alpha.25",
"@backstage/plugin-techdocs": "^0.1.1-alpha.25",
"@backstage/theme": "^0.1.1-alpha.25",
"@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",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
@@ -41,18 +41,18 @@
"swr": "^0.3.0"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.25",
"@backstage/dev-utils": "^0.1.1-alpha.25",
"@backstage/test-utils": "^0.1.1-alpha.25",
"@backstage/cli": "^0.1.1-alpha.26",
"@backstage/dev-utils": "^0.1.1-alpha.26",
"@backstage/test-utils": "^0.1.1-alpha.26",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/react-hooks": "^3.3.0",
"@testing-library/user-event": "^12.0.7",
"@types/jest": "^26.0.7",
"@types/node": "^12.0.0",
"cross-fetch": "^3.0.6",
"msw": "^0.21.2",
"react-test-renderer": "^16.13.1",
"cross-fetch": "^3.0.6"
"react-test-renderer": "^16.13.1"
},
"files": [
"dist"
@@ -14,4 +14,16 @@
* limitations under the License.
*/
export { ExampleCostInsightsClient } from './ExampleCostInsightsClient';
import React from 'react';
import { renderInTestApp } from '@backstage/test-utils';
import { EntityNotFound } from './EntityNotFound';
describe('<EntityNotFound />', () => {
it('renders without exploding', async () => {
const { getByText } = await renderInTestApp(<EntityNotFound />);
expect(getByText(/entity was not found/i)).toBeInTheDocument();
expect(getByText(/getting started documentation/i)).toBeInTheDocument();
expect(getByText(/docs/i)).toBeInTheDocument();
});
});
@@ -25,12 +25,21 @@ const useStyles = makeStyles<BackstageTheme>(theme => ({
container: {
paddingTop: theme.spacing(24),
paddingLeft: theme.spacing(8),
[theme.breakpoints.down('xs')]: {
padding: theme.spacing(2),
},
},
title: {
paddingBottom: theme.spacing(2),
[theme.breakpoints.down('xs')]: {
fontSize: 32,
},
},
body: {
paddingBottom: theme.spacing(6),
[theme.breakpoints.down('xs')]: {
paddingBottom: theme.spacing(5),
},
},
}));
@@ -38,7 +47,7 @@ export const EntityNotFound = () => {
const classes = useStyles();
return (
<Grid container className={classes.container}>
<Grid container spacing={0} className={classes.container}>
<Illo />
<Grid item xs={12} sm={6}>
<Typography variant="h2" className={classes.title}>
@@ -18,16 +18,29 @@ import React from 'react';
import { makeStyles } from '@material-ui/core';
import IlloSvgUrl from './illo.svg';
const useStyles = makeStyles({
const useStyles = makeStyles(theme => ({
illo: {
maxWidth: '60%',
top: 100,
right: 20,
position: 'absolute',
[theme.breakpoints.down('xs')]: {
maxWidth: '96%',
position: 'relative',
top: 'unset',
right: 'unset',
margin: `${theme.spacing(10)}px auto ${theme.spacing(4)}px`,
},
},
});
}));
export const Illo = () => {
const classes = useStyles();
return <img src={IlloSvgUrl} className={classes.illo} alt="" />;
return (
<img
src={IlloSvgUrl}
className={classes.illo}
alt="Illustration on entity not found page"
/>
);
};

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

@@ -13,5 +13,4 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { default } from './UnlabeledDataflowBarChart';
export { Illo } from './Illo';
+9 -9
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-circleci",
"version": "0.1.1-alpha.25",
"version": "0.1.1-alpha.26",
"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.25",
"@backstage/core": "^0.1.1-alpha.25",
"@backstage/plugin-catalog": "^0.1.1-alpha.25",
"@backstage/theme": "^0.1.1-alpha.25",
"@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",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
@@ -38,17 +38,17 @@
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.25",
"@backstage/dev-utils": "^0.1.1-alpha.25",
"@backstage/cli": "^0.1.1-alpha.26",
"@backstage/dev-utils": "^0.1.1-alpha.26",
"@backstage/test-utils": "^0.1.1-alpha.26",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
"@types/jest": "^26.0.7",
"@types/node": "^12.0.0",
"@types/react-lazylog": "^4.5.0",
"msw": "^0.21.2",
"cross-fetch": "^3.0.6",
"@backstage/test-utils": "^0.1.1-alpha.25"
"msw": "^0.21.2"
},
"files": [
"dist"
+9 -9
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-cloudbuild",
"version": "0.1.1-alpha.25",
"version": "0.1.1-alpha.26",
"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.25",
"@backstage/core": "^0.1.1-alpha.25",
"@backstage/plugin-catalog": "^0.1.1-alpha.25",
"@backstage/theme": "^0.1.1-alpha.25",
"@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",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
@@ -39,16 +39,16 @@
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.25",
"@backstage/dev-utils": "^0.1.1-alpha.25",
"@backstage/cli": "^0.1.1-alpha.26",
"@backstage/dev-utils": "^0.1.1-alpha.26",
"@backstage/test-utils": "^0.1.1-alpha.26",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
"@types/jest": "^26.0.7",
"@types/node": "^12.0.0",
"msw": "^0.21.2",
"cross-fetch": "^3.0.6",
"@backstage/test-utils": "^0.1.1-alpha.25"
"msw": "^0.21.2"
},
"files": [
"dist"
+16 -3
View File
@@ -13,8 +13,21 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createDevApp } from '@backstage/dev-utils';
import { plugin } from '../src/plugin';
import { createPlugin, createApiFactory } from '@backstage/core';
import { ExampleCostInsightsClient } from '../src/api';
import { costInsightsApiRef } from '../src';
import { pluginConfig } from '../src/plugin';
createDevApp().registerPlugin(plugin).render();
const devPlugin = createPlugin({
...pluginConfig,
apis: [
createApiFactory({
api: costInsightsApiRef,
deps: {},
factory: () => new ExampleCostInsightsClient(),
}),
],
});
createDevApp().registerPlugin(devPlugin).render();
+13 -10
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-cost-insights",
"version": "0.1.1-alpha.25",
"version": "0.1.1-alpha.26",
"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.25",
"@backstage/core": "^0.1.1-alpha.25",
"@backstage/test-utils": "^0.1.1-alpha.25",
"@backstage/theme": "^0.1.1-alpha.25",
"@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",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
@@ -33,6 +33,7 @@
"@types/recharts": "^1.8.14",
"canvas": "^2.6.1",
"classnames": "^2.2.6",
"dayjs": "^1.9.4",
"history": "^5.0.0",
"moment": "^2.27.0",
"qs": "^6.9.4",
@@ -41,21 +42,23 @@
"react-router-dom": "6.0.0-beta.0",
"react-use": "^15.3.3",
"recharts": "^1.8.5",
"regression": "^2.0.1",
"yup": "^0.29.3"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.25",
"@backstage/dev-utils": "^0.1.1-alpha.25",
"@backstage/cli": "^0.1.1-alpha.26",
"@backstage/dev-utils": "^0.1.1-alpha.26",
"@backstage/test-utils": "^0.1.1-alpha.26",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
"@types/jest": "^26.0.7",
"@types/node": "^12.0.0",
"@types/yup": "^0.29.8",
"@types/recharts": "^1.8.14",
"msw": "^0.21.2",
"@types/regression": "^2.0.0",
"@types/yup": "^0.29.8",
"cross-fetch": "^3.0.6",
"@backstage/test-utils": "^0.1.1-alpha.25"
"msw": "^0.21.2"
},
"files": [
"dist"
@@ -17,11 +17,11 @@
import dayjs from 'dayjs';
import regression, { DataPoint } from 'regression';
import { CostInsightsApi } from './CostInsightsApi';
import {
Alert,
ChangeStatistic,
Cost,
CostInsightsApi,
DateAggregation,
DEFAULT_DATE_FORMAT,
Duration,
@@ -37,7 +37,7 @@ import {
Trendline,
UnlabeledDataflowAlert,
UnlabeledDataflowData,
} from '@backstage/plugin-cost-insights';
} from '../types';
type IntervalFields = {
duration: Duration;
+1
View File
@@ -15,3 +15,4 @@
*/
export * from './CostInsightsApi';
export * from './ExampleCostInsightsClient';
@@ -16,7 +16,7 @@
import React from 'react';
import { renderInTestApp } from '@backstage/test-utils';
import AlertActionCard from './AlertActionCard';
import { AlertActionCard } from './AlertActionCard';
import { ProjectGrowthAlert, ProjectGrowthData } from '../../types';
import { MockScrollProvider } from '../../utils/tests';
@@ -27,7 +27,7 @@ type AlertActionCardProps = {
number: number;
};
const AlertActionCard = ({ alert, number }: AlertActionCardProps) => {
export const AlertActionCard = ({ alert, number }: AlertActionCardProps) => {
const { scrollIntoView } = useScroll(`alert-${number}`);
const headerClasses = useHeaderStyles();
const classes = useStyles();
@@ -43,5 +43,3 @@ const AlertActionCard = ({ alert, number }: AlertActionCardProps) => {
</Card>
);
};
export default AlertActionCard;
@@ -15,14 +15,14 @@
*/
import React, { FC, Fragment } from 'react';
import { Paper, Divider } from '@material-ui/core';
import AlertActionCard from './AlertActionCard';
import { AlertActionCard } from './AlertActionCard';
import { Alert } from '../../types';
type AlertActionCardList = {
alerts: Array<Alert>;
};
const AlertActionCardList: FC<AlertActionCardList> = ({ alerts }) => (
export const AlertActionCardList: FC<AlertActionCardList> = ({ alerts }) => (
<Paper>
{alerts.map((alert, index) => (
<Fragment key={`alert-${index}`}>
@@ -32,5 +32,3 @@ const AlertActionCardList: FC<AlertActionCardList> = ({ alerts }) => (
))}
</Paper>
);
export default AlertActionCardList;
@@ -14,4 +14,4 @@
* limitations under the License.
*/
export { default } from './AlertActionCardList';
export { AlertActionCardList } from './AlertActionCardList';
@@ -16,8 +16,8 @@
import React from 'react';
import { Grid } from '@material-ui/core';
import AlertInsightsSection from './AlertInsightsSection';
import AlertInsightsHeader from './AlertInsightsHeader';
import { AlertInsightsSection } from './AlertInsightsSection';
import { AlertInsightsHeader } from './AlertInsightsHeader';
import { Alert } from '../../types';
const title = "Your team's action items";
@@ -28,7 +28,7 @@ type AlertInsightsProps = {
alerts: Array<Alert>;
};
const AlertInsights = ({ alerts }: AlertInsightsProps) => (
export const AlertInsights = ({ alerts }: AlertInsightsProps) => (
<Grid container direction="column" spacing={2}>
<Grid item>
<AlertInsightsHeader title={title} subtitle={subtitle} />
@@ -42,5 +42,3 @@ const AlertInsights = ({ alerts }: AlertInsightsProps) => (
</Grid>
</Grid>
);
export default AlertInsights;
@@ -25,7 +25,10 @@ type AlertInsightsHeaderProps = {
subtitle: string;
};
const AlertInsightsHeader = ({ title, subtitle }: AlertInsightsHeaderProps) => {
export const AlertInsightsHeader = ({
title,
subtitle,
}: AlertInsightsHeaderProps) => {
const classes = useStyles();
const { ScrollAnchor } = useScroll(DefaultNavigation.AlertInsightsHeader);
return (
@@ -43,5 +46,3 @@ const AlertInsightsHeader = ({ title, subtitle }: AlertInsightsHeaderProps) => {
</Box>
);
};
export default AlertInsightsHeader;
@@ -15,7 +15,7 @@
*/
import React from 'react';
import { Box, Button } from '@material-ui/core';
import AlertInsightsSectionHeader from './AlertInsightsSectionHeader';
import { AlertInsightsSectionHeader } from './AlertInsightsSectionHeader';
import { Alert } from '../../types';
type AlertInsightsSectionProps = {
@@ -23,7 +23,10 @@ type AlertInsightsSectionProps = {
number: number;
};
const AlertInsightsSection = ({ alert, number }: AlertInsightsSectionProps) => {
export const AlertInsightsSection = ({
alert,
number,
}: AlertInsightsSectionProps) => {
return (
<Box display="flex" flexDirection="column">
<AlertInsightsSectionHeader
@@ -41,5 +44,3 @@ const AlertInsightsSection = ({ alert, number }: AlertInsightsSectionProps) => {
</Box>
);
};
export default AlertInsightsSection;
@@ -25,7 +25,7 @@ type AlertInsightsSectionHeaderProps = {
subtitle: string;
};
const AlertInsightsSectionHeader = ({
export const AlertInsightsSectionHeader = ({
number,
title,
subtitle,
@@ -47,5 +47,3 @@ const AlertInsightsSectionHeader = ({
</Box>
);
};
export default AlertInsightsSectionHeader;
@@ -14,4 +14,4 @@
* limitations under the License.
*/
export { default } from './AlertInsights';
export { AlertInsights } from './AlertInsights';
@@ -32,7 +32,7 @@ type AlertInstructionsLayoutProps = {
title: string;
};
const AlertInstructionsLayout = ({
export const AlertInstructionsLayout = ({
title,
children,
}: PropsWithChildren<AlertInstructionsLayoutProps>) => {
@@ -65,5 +65,3 @@ const AlertInstructionsLayout = ({
</CostInsightsThemeProvider>
);
};
export default AlertInstructionsLayout;
@@ -14,4 +14,4 @@
* limitations under the License.
*/
export { default } from './AlertInstructionsLayout';
export { AlertInstructionsLayout } from './AlertInstructionsLayout';
@@ -17,7 +17,7 @@
import React from 'react';
import { TooltipPayload } from 'recharts';
import { fireEvent } from '@testing-library/react';
import BarChart, { BarChartProps } from './BarChart';
import { BarChart, BarChartProps } from './BarChart';
import { BarChartData, ResourceData } from '../../types';
import { createMockEntity } from '../../utils/mockData';
import { resourceSort } from '../../utils/sort';
@@ -28,8 +28,8 @@ import {
TooltipPayload,
} from 'recharts';
import { Box, useTheme } from '@material-ui/core';
import BarChartTick from './BarChartTick';
import BarChartStepper from './BarChartStepper';
import { BarChartTick } from './BarChartTick';
import { BarChartStepper } from './BarChartStepper';
import { Tooltip, TooltipItemProps } from '../Tooltip';
import { currencyFormatter } from '../../utils/formatters';
@@ -52,7 +52,7 @@ export type BarChartProps = {
resources: ResourceData[];
};
const BarChart = ({
export const BarChart = ({
responsive = true,
displayAmount = 6,
barChartData,
@@ -171,5 +171,3 @@ const BarChart = ({
</Box>
);
};
export default BarChart;
@@ -25,7 +25,7 @@ type BarChartLabel = {
width: number;
};
const BarChartLabel = ({
export const BarChartLabel = ({
x,
y,
height,
@@ -52,5 +52,3 @@ const BarChartLabel = ({
</foreignObject>
);
};
export default BarChartLabel;
@@ -18,8 +18,8 @@ import React, { useEffect, useState } from 'react';
import { Paper, Slide } from '@material-ui/core';
import ChevronLeftIcon from '@material-ui/icons/ChevronLeft';
import ChevronRightIcon from '@material-ui/icons/ChevronRight';
import BarChartStepperButton from './BarChartStepperButton';
import BarChartSteps from './BarChartSteps';
import { BarChartStepperButton } from './BarChartStepperButton';
import { BarChartSteps } from './BarChartSteps';
import { useBarChartStepperStyles } from '../../utils/styles';
type BarChartStepperProps = {
@@ -28,7 +28,7 @@ type BarChartStepperProps = {
onChange: (activeStep: number) => void;
};
const BarChartStepper = ({
export const BarChartStepper = ({
steps,
disableScroll,
onChange,
@@ -111,5 +111,3 @@ const BarChartStepper = ({
</Paper>
);
};
export default BarChartStepper;
@@ -22,7 +22,7 @@ interface BarChartStepperButtonProps extends ButtonBaseProps {
name: string;
}
const BarChartStepperButton = forwardRef(
export const BarChartStepperButton = forwardRef(
(
{
name,
@@ -45,5 +45,3 @@ const BarChartStepperButton = forwardRef(
);
},
);
export default BarChartStepperButton;
@@ -24,7 +24,11 @@ export type BarChartSteps = {
onClick: (index: number) => void;
};
const BarChartSteps = ({ steps, activeStep, onClick }: BarChartSteps) => {
export const BarChartSteps = ({
steps,
activeStep,
onClick,
}: BarChartSteps) => {
const classes = useStyles();
const handleOnClick = (index: number) => (
event: React.MouseEvent<HTMLButtonElement, MouseEvent>,
@@ -48,5 +52,3 @@ const BarChartSteps = ({ steps, activeStep, onClick }: BarChartSteps) => {
</div>
);
};
export default BarChartSteps;
@@ -15,7 +15,7 @@
*/
import React from 'react';
import BarChartLabel from './BarChartLabel';
import { BarChartLabel } from './BarChartLabel';
type BarChartTickProps = {
x: number;
@@ -44,5 +44,3 @@ export const BarChartTick = ({
</BarChartLabel>
);
};
export default BarChartTick;
@@ -14,4 +14,5 @@
* limitations under the License.
*/
export { default } from './BarChart';
export { BarChart } from './BarChart';
export type { BarChartProps } from './BarChart';

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