diff --git a/.changeset/green-otters-grin.md b/.changeset/green-otters-grin.md
new file mode 100644
index 0000000000..abfd332a2b
--- /dev/null
+++ b/.changeset/green-otters-grin.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-catalog-backend': patch
+---
+
+Add support for configure an LDAP query filter on multiple lines.
diff --git a/.changeset/metal-foxes-speak.md b/.changeset/metal-foxes-speak.md
new file mode 100644
index 0000000000..45b8ae3c7c
--- /dev/null
+++ b/.changeset/metal-foxes-speak.md
@@ -0,0 +1,5 @@
+---
+'@backstage/catalog-model': patch
+---
+
+Add getEntitySourceLocation helper
diff --git a/.changeset/old-clouds-whisper.md b/.changeset/old-clouds-whisper.md
new file mode 100644
index 0000000000..edaba13ad7
--- /dev/null
+++ b/.changeset/old-clouds-whisper.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-kubernetes-backend': patch
+---
+
+Use `string` TypeScript type instead of `String`.
diff --git a/.changeset/rare-mayflies-remain.md b/.changeset/rare-mayflies-remain.md
new file mode 100644
index 0000000000..bb85afd344
--- /dev/null
+++ b/.changeset/rare-mayflies-remain.md
@@ -0,0 +1,5 @@
+---
+'@backstage/core': patch
+---
+
+Pass `inverse` prop to Gauge from GaugeCard
diff --git a/.changeset/sour-dryers-sort.md b/.changeset/sour-dryers-sort.md
new file mode 100644
index 0000000000..ee15655e82
--- /dev/null
+++ b/.changeset/sour-dryers-sort.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-api-docs': patch
+---
+
+chore(deps): bump @asyncapi/react-component from 0.19.2 to 0.22.3
diff --git a/.changeset/techdocs-chatty-impalas-compare.md b/.changeset/techdocs-chatty-impalas-compare.md
new file mode 100644
index 0000000000..26e7fd571d
--- /dev/null
+++ b/.changeset/techdocs-chatty-impalas-compare.md
@@ -0,0 +1,6 @@
+---
+'@backstage/plugin-techdocs': patch
+---
+
+Reworked the TechDocs plugin to support using the configured company name instead of
+'Backstage' in the page title.
diff --git a/.changeset/techdocs-dry-starfishes-carry.md b/.changeset/techdocs-dry-starfishes-carry.md
new file mode 100644
index 0000000000..f5841c6994
--- /dev/null
+++ b/.changeset/techdocs-dry-starfishes-carry.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-techdocs': patch
+---
+
+Add borders to TechDocs tables and increase font size. Fixes #5264 and #5276.
diff --git a/.changeset/thick-frogs-hang.md b/.changeset/thick-frogs-hang.md
new file mode 100644
index 0000000000..44add55206
--- /dev/null
+++ b/.changeset/thick-frogs-hang.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-scaffolder-backend': patch
+---
+
+Fix execution of the GitHub Pull Request publish action on Windows.
diff --git a/.changeset/tiny-games-hide.md b/.changeset/tiny-games-hide.md
new file mode 100644
index 0000000000..2687174060
--- /dev/null
+++ b/.changeset/tiny-games-hide.md
@@ -0,0 +1,5 @@
+---
+'@backstage/catalog-client': patch
+---
+
+Make sure the `CatalogClient` escapes URL parameters correctly.
diff --git a/.changeset/witty-hotels-know.md b/.changeset/witty-hotels-know.md
new file mode 100644
index 0000000000..4b20111fa7
--- /dev/null
+++ b/.changeset/witty-hotels-know.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-tech-radar': patch
+---
+
+Add markdown support for tech radar entry description
diff --git a/.changeset/young-kids-remember.md b/.changeset/young-kids-remember.md
new file mode 100644
index 0000000000..c07097234f
--- /dev/null
+++ b/.changeset/young-kids-remember.md
@@ -0,0 +1,5 @@
+---
+'@backstage/cli': patch
+---
+
+Disable hot reloading in CI environments.
diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt
index 8c385e1bbb..226af9bf0a 100644
--- a/.github/styles/vocab.txt
+++ b/.github/styles/vocab.txt
@@ -114,6 +114,7 @@ dataflow
deadnaming
debounce
declaratively
+deps
destructured
dev
devops
diff --git a/contrib/docs/tutorials/aws-alb-aad-oidc-auth.md b/contrib/docs/tutorials/aws-alb-aad-oidc-auth.md
new file mode 100644
index 0000000000..ba0aef89ea
--- /dev/null
+++ b/contrib/docs/tutorials/aws-alb-aad-oidc-auth.md
@@ -0,0 +1,170 @@
+# Using AWS Application Load Balancer with Azure Active Directory to authenticate requests
+
+Backstage allows offloading the responsibility of authenticating users to an AWS Application Load Balancer (**ALB**), leveraging the authentication support on ALB.
+This tutorial shows how to use authentication on an ALB sitting in front of Backstage.
+Azure Active Directory (**AAD**) is used as identity provider but any identity provider supporting OpenID Connect (OIDC) can be used.
+
+It is assumed an ALB is already serving traffic in front of a Backstage instance configured to serve the frontend app from the backend.
+
+## Infrastructure setup
+
+### AAD App
+
+The AAD App is used to execute the authentication flow, serve and refresh the identity token.
+
+Create the AAD App following the steps outlined in `Create a Microsoft App Registration in Microsoft Portal` section from the tutorial [Monorepo App Setup With Authentication][monorepo-app-setup-with-auth].
+
+Instead of `localhost` addresses, use the following values.
+
+- Identifier URI: `https://backstage.yourdomain.com`
+- Redirect URI: `https://backstage.yourdomain.com/oauth2/idpresponse`
+
+`Application (client) Id`, `Directory (tenant) ID` and `client secret`values will be used while configuring the ALB.
+
+### ALB
+
+In the AWS console, configure ALB Authentication:
+
+- Edit the ALB rule used to forward the traffic to Backstage and add a new `Authenticate` action. The action will have higher priority compared to the existing `Forward to`.
+- Select `OIDC` under `Authenticate`
+- Set `Issuer` to `https://login.microsoftonline.com/{TENANT_ID}/v2.0`, replacing `{TENANT_ID}` with the value of `Directory (tenant) ID` of the AAD App.
+- Set `Authorization endpoint` to `https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/authorize`, replacing `{TENANT_ID}` with the value of `Directory (tenant) ID` of the AAD App.
+- Set `Token endpoint` to `https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/authorize`, replacing `{TENANT_ID}` with the value of `Directory (tenant) ID` of the AAD App.
+- Set `User info endpoint` to `https://graph.microsoft.com/oidc/userinfo`
+- Set `Client ID` to the AAD App `Application (client) Id`
+- Set `Client secret` to the AAD APP `client secret`
+
+Use the following advanced settings:
+
+- `Session cookie name` = `AWSELBAuthSessionCookie`
+- `Session timeout` = `604800` seconds
+- `Scope` = `openid profile offline_access`
+- `Action on unauthenticated request` = `Autenticate (client reattempt)`
+
+Once you've saved the action, you should see an authentication flow be triggered against AAD when visiting Backstage address at `https://backstage.yourdomain.com`. The flow will not complete successfully as the Backstage app isn't yet configured properly.
+
+## Backstage changes
+
+### Frontend
+
+The Backstage App needs a SignInPage when authentication is required.
+When using ALB authentication Backstage will only be loaded once the user has successfully authenticated; we won't need to display a SignIn page, however we will need to create a dummy SignIn component that can refresh the token.
+
+- edit `packages/app/src/App.tsx`
+- import the following two additional definitions from `@backstage/core`: `useApi`, `configApiRef`; these will be used to check whether Backstage is running locally or behind an ALB
+- add the following definition just before the app is created (`const app = createApp`):
+
+```ts
+const DummySignInComponent: any = (props: any) => {
+ try {
+ const config = useApi(configApiRef);
+ const shouldAuth = !!config.getOptionalConfig('auth.providers.awsalb');
+ if (shouldAuth) {
+ fetch(`${window.location.origin}/api/auth/awsalb/refresh`)
+ .then(data => data.json())
+ .then(data => {
+ props.onResult({
+ userId: data.backstageIdentity.id,
+ profile: data.profile,
+ });
+ });
+ } else {
+ props.onResult({
+ userId: 'guest',
+ profile: {
+ email: 'guest@example.com',
+ displayName: 'Guest',
+ picture: '',
+ },
+ });
+ }
+ return
;
+ } catch (err) {
+ return
{err.message}
;
+ }
+};
+```
+
+- add `DummySignInComponent` as `SignInPage`:
+
+```ts
+const app = createApp({
+ ...
+ components: {
+ SignInPage: DummySignInComponent,
+ ...
+ },
+ ...
+});
+```
+
+### Backend
+
+When using ALB auth it is not possible to leverage the built-in auth config discovery mechanism implemented in the app created by default; bespoke logic needs to be implemented.
+
+- replace the content of `packages/backend/plugin/auth.ts` with the below
+
+```ts
+import {
+ createRouter,
+ AuthResponse,
+ AuthProviderFactoryOptions,
+ defaultAuthProviderFactories,
+} from '@backstage/plugin-auth-backend';
+import { PluginEnvironment } from '../types';
+
+export default async function createPlugin({
+ logger,
+ database,
+ config,
+ discovery,
+}: PluginEnvironment) {
+ const identityResolver = (payload: any): Promise> => {
+ return Promise.resolve({
+ providerInfo: {},
+ profile: {
+ email: payload.email,
+ displayName: payload.name,
+ picture: payload.picture,
+ },
+ backstageIdentity: {
+ id: payload.email,
+ },
+ });
+ };
+ const providerFactories = {
+ awsalb: (options: AuthProviderFactoryOptions) =>
+ defaultAuthProviderFactories.awsalb({ ...options, identityResolver }),
+ };
+ return await createRouter({
+ logger,
+ config,
+ database,
+ discovery,
+ providerFactories,
+ });
+}
+```
+
+### Configuration
+
+Use the following `auth` configuration when running Backstage on AWS:
+
+```yaml
+auth:
+ providers:
+ awsalb:
+ issuer:
+ issuer: https://login.microsoftonline.com/{TENANT_ID}/v2.0
+ region: { AWS_REGION }
+```
+
+Replace `{TENANT_ID}` with the value of `Directory (tenant) ID` of the AAD App and `{AWS_REGION}` with the AWS region identifier where the ALB is deployed (for example: `eu-central-1`).
+
+## Conclusion
+
+Once it's deployed, after going through the AAD authentication flow, Backstage should display the AAD user details.
+
+
+
+[monorepo-app-setup-with-auth-ms]: https://backstage.io/docs/tutorials/quickstart-app-auth#the-auth-configuration
diff --git a/docs/features/software-catalog/well-known-annotations.md b/docs/features/software-catalog/well-known-annotations.md
index 8ec4813799..0053c14b79 100644
--- a/docs/features/software-catalog/well-known-annotations.md
+++ b/docs/features/software-catalog/well-known-annotations.md
@@ -92,12 +92,14 @@ view and edit links need changing.
# Example:
metadata:
annotations:
- backstage.io/source-location: github:https://github.com/my-org/my-service
+ backstage.io/source-location: url:https://github.com/my-org/my-service/
```
A `Location` reference that points to the source code of the entity (typically a
`Component`). Useful when catalog files do not get ingested from the source code
-repository itself.
+repository itself. If the URL points to a folder, it is important that it is
+suffixed with a `'/'` in order for relative path resolution to work
+consistently.
### jenkins.io/github-folder
diff --git a/microsite/pages/en/index.js b/microsite/pages/en/index.js
index 47f9568d9b..a761d6b6e2 100644
--- a/microsite/pages/en/index.js
+++ b/microsite/pages/en/index.js
@@ -292,7 +292,7 @@ class Index extends React.Component {
Build your own software templates
Contribute
diff --git a/packages/app/package.json b/packages/app/package.json
index 2bac2e6e09..1fc0335aa2 100644
--- a/packages/app/package.json
+++ b/packages/app/package.json
@@ -42,10 +42,10 @@
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@octokit/rest": "^18.0.12",
- "@roadiehq/backstage-plugin-buildkite": "^0.3.0",
- "@roadiehq/backstage-plugin-github-insights": "^0.3.3",
- "@roadiehq/backstage-plugin-github-pull-requests": "^0.7.9",
- "@roadiehq/backstage-plugin-travis-ci": "^0.4.7",
+ "@roadiehq/backstage-plugin-buildkite": "^1.0.0",
+ "@roadiehq/backstage-plugin-github-insights": "^1.0.0",
+ "@roadiehq/backstage-plugin-github-pull-requests": "^1.0.0",
+ "@roadiehq/backstage-plugin-travis-ci": "^1.0.0",
"history": "^5.0.0",
"prop-types": "^15.7.2",
"react": "^16.12.0",
diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx
index d8ee62a12c..d6f91957a9 100644
--- a/packages/app/src/components/catalog/EntityPage.tsx
+++ b/packages/app/src/components/catalog/EntityPage.tsx
@@ -81,27 +81,27 @@ import { EntitySentryContent } from '@backstage/plugin-sentry';
import { EntityTechdocsContent } from '@backstage/plugin-techdocs';
import { EntityTodoContent } from '@backstage/plugin-todo';
import { Button, Grid } from '@material-ui/core';
-// import {
-// EntityBuildkiteContent,
-// isBuildkiteAvailable,
-// } from '@roadiehq/backstage-plugin-buildkite';
-// import {
-// EntityGitHubInsightsContent,
-// EntityLanguagesCard,
-// EntityReadMeCard,
-// EntityReleasesCard,
-// isGithubInsightsAvailable,
-// } from '@roadiehq/backstage-plugin-github-insights';
-// import {
-// EntityGithubPullRequestsContent,
-// EntityGithubPullRequestsOverviewCard,
-// isGithubPullRequestsAvailable,
-// } from '@roadiehq/backstage-plugin-github-pull-requests';
-// import {
-// EntityTravisCIContent,
-// EntityTravisCIOverviewCard,
-// isTravisciAvailable,
-// } from '@roadiehq/backstage-plugin-travis-ci';
+import {
+ EntityBuildkiteContent,
+ isBuildkiteAvailable,
+} from '@roadiehq/backstage-plugin-buildkite';
+import {
+ EntityGithubInsightsContent,
+ EntityGithubInsightsLanguagesCard,
+ EntityGithubInsightsReadmeCard,
+ EntityGithubInsightsReleasesCard,
+ isGithubInsightsAvailable,
+} from '@roadiehq/backstage-plugin-github-insights';
+import {
+ EntityGithubPullRequestsContent,
+ EntityGithubPullRequestsOverviewCard,
+ isGithubPullRequestsAvailable,
+} from '@roadiehq/backstage-plugin-github-pull-requests';
+import {
+ EntityTravisCIContent,
+ EntityTravisCIOverviewCard,
+ isTravisciAvailable,
+} from '@roadiehq/backstage-plugin-travis-ci';
const EntityLayoutWrapper = (props: { children?: ReactNode }) => {
const [badgesDialogOpen, setBadgesDialogOpen] = useState(false);
@@ -135,9 +135,9 @@ export const cicdContent = (
- {/*
+
- */}
+
@@ -147,9 +147,9 @@ export const cicdContent = (
- {/*
+
- */}
+
@@ -182,11 +182,11 @@ const cicdCard = (
- {/*
+
- */}
+
@@ -228,17 +228,17 @@ const overviewContent = (
{cicdCard}
- {/*
+ Boolean(isGithubInsightsAvailable(e))}>
-
-
+
+
-
+
- */}
+
@@ -248,13 +248,13 @@ const overviewContent = (
- {/*
+ Boolean(isGithubPullRequestsAvailable(e))}>
- */}
+
@@ -295,13 +295,13 @@ const serviceEntityPage = (
- {/*
+
- */}
+
- {/*
-
- */}
+
+
+
@@ -339,13 +339,13 @@ const websiteEntityPage = (
- {/*
+
- */}
+
- {/*
+
- */}
+
diff --git a/packages/app/src/plugins.ts b/packages/app/src/plugins.ts
index ccc7e2a45c..e08bfd6abe 100644
--- a/packages/app/src/plugins.ts
+++ b/packages/app/src/plugins.ts
@@ -27,19 +27,19 @@ export { plugin as GraphiQL } from '@backstage/plugin-graphiql';
export { plugin as GithubActions } from '@backstage/plugin-github-actions';
export { plugin as Rollbar } from '@backstage/plugin-rollbar';
export { plugin as Newrelic } from '@backstage/plugin-newrelic';
-export { plugin as TravisCI } from '@roadiehq/backstage-plugin-travis-ci';
+export { travisciPlugin } from '@roadiehq/backstage-plugin-travis-ci';
export { plugin as Jenkins } from '@backstage/plugin-jenkins';
export { plugin as ApiDocs } from '@backstage/plugin-api-docs';
-export { plugin as GithubPullRequests } from '@roadiehq/backstage-plugin-github-pull-requests';
+export { githubPullRequestsPlugin } from '@roadiehq/backstage-plugin-github-pull-requests';
export { plugin as GcpProjects } from '@backstage/plugin-gcp-projects';
export { plugin as Kubernetes } from '@backstage/plugin-kubernetes';
export { plugin as Cloudbuild } from '@backstage/plugin-cloudbuild';
export { plugin as CostInsights } from '@backstage/plugin-cost-insights';
-export { plugin as GitHubInsights } from '@roadiehq/backstage-plugin-github-insights';
+export { githubInsightsPlugin } from '@roadiehq/backstage-plugin-github-insights';
export { plugin as CatalogImport } from '@backstage/plugin-catalog-import';
export { plugin as UserSettings } from '@backstage/plugin-user-settings';
export { plugin as PagerDuty } from '@backstage/plugin-pagerduty';
-export { plugin as Buildkite } from '@roadiehq/backstage-plugin-buildkite';
+export { buildkitePlugin } from '@roadiehq/backstage-plugin-buildkite';
export { plugin as Search } from '@backstage/plugin-search';
export { plugin as Org } from '@backstage/plugin-org';
export { plugin as Kafka } from '@backstage/plugin-kafka';
diff --git a/packages/catalog-client/src/CatalogClient.ts b/packages/catalog-client/src/CatalogClient.ts
index 7426aabaac..3de25e9808 100644
--- a/packages/catalog-client/src/CatalogClient.ts
+++ b/packages/catalog-client/src/CatalogClient.ts
@@ -42,10 +42,14 @@ export class CatalogClient implements CatalogApi {
}
async getLocationById(
- id: String,
+ id: string,
options?: CatalogRequestOptions,
): Promise {
- return await this.requestOptional('GET', `/locations/${id}`, options);
+ return await this.requestOptional(
+ 'GET',
+ `/locations/${encodeURIComponent(id)}`,
+ options,
+ );
}
async getEntities(
@@ -86,7 +90,9 @@ export class CatalogClient implements CatalogApi {
const { kind, namespace = 'default', name } = compoundName;
return this.requestOptional(
'GET',
- `/entities/by-name/${kind}/${namespace}/${name}`,
+ `/entities/by-name/${encodeURIComponent(kind)}/${encodeURIComponent(
+ namespace,
+ )}/${encodeURIComponent(name)}`,
options,
);
}
@@ -171,14 +177,22 @@ export class CatalogClient implements CatalogApi {
id: string,
options?: CatalogRequestOptions,
): Promise {
- await this.requestIgnored('DELETE', `/locations/${id}`, options);
+ await this.requestIgnored(
+ 'DELETE',
+ `/locations/${encodeURIComponent(id)}`,
+ options,
+ );
}
async removeEntityByUid(
uid: string,
options?: CatalogRequestOptions,
): Promise {
- await this.requestIgnored('DELETE', `/entities/by-uid/${uid}`, options);
+ await this.requestIgnored(
+ 'DELETE',
+ `/entities/by-uid/${encodeURIComponent(uid)}`,
+ options,
+ );
}
//
diff --git a/packages/catalog-client/src/types.ts b/packages/catalog-client/src/types.ts
index 04cbb2b689..0d25bf7483 100644
--- a/packages/catalog-client/src/types.ts
+++ b/packages/catalog-client/src/types.ts
@@ -46,7 +46,7 @@ export interface CatalogApi {
// Locations
getLocationById(
- id: String,
+ id: string,
options?: CatalogRequestOptions,
): Promise;
getOriginLocationByEntity(
diff --git a/packages/catalog-model/src/location/helpers.test.ts b/packages/catalog-model/src/location/helpers.test.ts
index 888c79b328..3b5994b956 100644
--- a/packages/catalog-model/src/location/helpers.test.ts
+++ b/packages/catalog-model/src/location/helpers.test.ts
@@ -14,7 +14,11 @@
* limitations under the License.
*/
-import { parseLocationReference, stringifyLocationReference } from './helpers';
+import {
+ getEntitySourceLocation,
+ parseLocationReference,
+ stringifyLocationReference,
+} from './helpers';
describe('parseLocationReference', () => {
it('works for the simple case', () => {
@@ -68,3 +72,48 @@ describe('stringifyLocationReference', () => {
).toThrow('Unable to stringify location reference, empty target');
});
});
+
+describe('getEntitySourceLocation', () => {
+ it('returns the source-location', () => {
+ expect(
+ getEntitySourceLocation({
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'Location',
+ metadata: {
+ name: 'test',
+ namespace: 'default',
+ annotations: {
+ 'backstage.io/source-location': 'url:https://backstage.io/foo.yaml',
+ 'backstage.io/managed-by-location': 'url:https://spotify.com',
+ },
+ },
+ }),
+ ).toEqual({ target: 'https://backstage.io/foo.yaml', type: 'url' });
+ });
+
+ it('returns the managed-by-location', () => {
+ expect(
+ getEntitySourceLocation({
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'Location',
+ metadata: {
+ name: 'test',
+ namespace: 'default',
+ annotations: {
+ 'backstage.io/managed-by-location': 'url:https://spotify.com',
+ },
+ },
+ }),
+ ).toEqual({ target: 'https://spotify.com', type: 'url' });
+ });
+
+ it('rejects missing location annotation', () => {
+ expect(() =>
+ getEntitySourceLocation({
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'Location',
+ metadata: { name: 'test', namespace: 'default' },
+ }),
+ ).toThrow(`Entity 'location:default/test' is missing location`);
+ });
+});
diff --git a/packages/catalog-model/src/location/helpers.ts b/packages/catalog-model/src/location/helpers.ts
index fb1be5abad..5eff598c87 100644
--- a/packages/catalog-model/src/location/helpers.ts
+++ b/packages/catalog-model/src/location/helpers.ts
@@ -14,6 +14,9 @@
* limitations under the License.
*/
+import { Entity, stringifyEntityRef } from '../entity';
+import { LOCATION_ANNOTATION, SOURCE_LOCATION_ANNOTATION } from './annotation';
+
/**
* Parses a string form location reference.
*
@@ -80,3 +83,26 @@ export function stringifyLocationReference(ref: {
return `${type}:${target}`;
}
+
+/**
+ * Returns the source code location of the Entity, to the extent that one exists.
+ *
+ * If the returned location type is of type 'url', the target should be readable at least
+ * using the UrlReader from @backstage/backend-common. If it is not of type 'url', the caller
+ * needs to have explicit handling of each location type or signal that it is not supported.
+ */
+export function getEntitySourceLocation(
+ entity: Entity,
+): { type: string; target: string } {
+ const locationRef =
+ entity.metadata?.annotations?.[SOURCE_LOCATION_ANNOTATION] ??
+ entity.metadata?.annotations?.[LOCATION_ANNOTATION];
+
+ if (!locationRef) {
+ throw new Error(
+ `Entity '${stringifyEntityRef(entity)}' is missing location`,
+ );
+ }
+
+ return parseLocationReference(locationRef);
+}
diff --git a/packages/catalog-model/src/location/index.ts b/packages/catalog-model/src/location/index.ts
index fddc8bde37..751172c6ba 100644
--- a/packages/catalog-model/src/location/index.ts
+++ b/packages/catalog-model/src/location/index.ts
@@ -19,7 +19,11 @@ export {
ORIGIN_LOCATION_ANNOTATION,
SOURCE_LOCATION_ANNOTATION,
} from './annotation';
-export { parseLocationReference, stringifyLocationReference } from './helpers';
+export {
+ parseLocationReference,
+ stringifyLocationReference,
+ getEntitySourceLocation,
+} from './helpers';
export type { Location, LocationSpec } from './types';
export {
analyzeLocationSchema,
diff --git a/packages/cli/src/lib/bundler/server.ts b/packages/cli/src/lib/bundler/server.ts
index 5fc3a7b5de..24a24d25d2 100644
--- a/packages/cli/src/lib/bundler/server.ts
+++ b/packages/cli/src/lib/bundler/server.ts
@@ -43,7 +43,7 @@ export async function serveBundle(options: ServeOptions) {
const compiler = webpack(config);
const server = new WebpackDevServer(compiler, {
- hot: true,
+ hot: !process.env.CI,
contentBase: paths.targetPublic,
contentBasePublicPath: config.output?.publicPath,
publicPath: config.output?.publicPath,
diff --git a/packages/core/src/components/ProgressBars/GaugeCard.stories.tsx b/packages/core/src/components/ProgressBars/GaugeCard.stories.tsx
index 982999f8d6..5d54e5eda6 100644
--- a/packages/core/src/components/ProgressBars/GaugeCard.stories.tsx
+++ b/packages/core/src/components/ProgressBars/GaugeCard.stories.tsx
@@ -45,6 +45,9 @@ export const Default = () => (
+
+
+
);
@@ -67,6 +70,14 @@ export const Subhead = () => (
progress={0.89}
/>
+
+
+
);
@@ -81,5 +92,8 @@ export const LinkInFooter = () => (
+
+
+
);
diff --git a/packages/core/src/components/ProgressBars/GaugeCard.tsx b/packages/core/src/components/ProgressBars/GaugeCard.tsx
index 2154619b01..8dcec129c5 100644
--- a/packages/core/src/components/ProgressBars/GaugeCard.tsx
+++ b/packages/core/src/components/ProgressBars/GaugeCard.tsx
@@ -26,6 +26,7 @@ type Props = {
variant?: InfoCardVariants;
/** Progress in % specified as decimal, e.g. "0.23" */
progress: number;
+ inverse?: boolean;
deepLink?: BottomLinkProps;
};
@@ -38,7 +39,7 @@ const useStyles = makeStyles({
export const GaugeCard = (props: Props) => {
const classes = useStyles(props);
- const { title, subheader, progress, deepLink, variant } = props;
+ const { title, subheader, progress, inverse, deepLink, variant } = props;
return (
+
+ );
+}
diff --git a/plugins/config-schema/src/components/SchemaView/ObjectView.tsx b/plugins/config-schema/src/components/SchemaView/ObjectView.tsx
new file mode 100644
index 0000000000..8c72e169d3
--- /dev/null
+++ b/plugins/config-schema/src/components/SchemaView/ObjectView.tsx
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2021 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Box, Typography } from '@material-ui/core';
+import React from 'react';
+import { ChildView } from './ChildView';
+import { MetadataView } from './MetadataView';
+import { SchemaViewProps } from './types';
+
+function isRequired(name: string, required?: boolean | string[]) {
+ if (required === true) {
+ return true;
+ }
+ if (Array.isArray(required)) {
+ return required.includes(name);
+ }
+ return false;
+}
+
+export function ObjectView({ path, depth, schema }: SchemaViewProps) {
+ const properties = Object.entries(schema.properties ?? {});
+ const patternProperties = Object.entries(schema.patternProperties ?? {});
+
+ return (
+ <>
+ {depth > 0 && (
+
+ {schema.description && (
+
+ {schema.description}
+
+ )}
+
+
+ )}
+ {properties.length > 0 && (
+ <>
+ {depth > 0 && Properties}
+ {properties.map(([name, propSchema], index) => (
+
+ ))}
+ >
+ )}
+ {patternProperties.length > 0 && (
+ <>
+ {depth > 0 && (
+ Pattern Properties
+ )}
+ {patternProperties.map(([name, propSchema], index) => (
+ ` : name}
+ depth={depth + 1}
+ schema={propSchema}
+ lastChild={index === patternProperties.length - 1}
+ required={isRequired(name, schema.required)}
+ />
+ ))}
+ >
+ )}
+ {schema.additionalProperties && schema.additionalProperties !== true && (
+ <>
+ Additional Properties
+
+ >
+ )}
+ >
+ );
+}
diff --git a/plugins/config-schema/src/components/SchemaView/ScalarView.tsx b/plugins/config-schema/src/components/SchemaView/ScalarView.tsx
new file mode 100644
index 0000000000..1349358abd
--- /dev/null
+++ b/plugins/config-schema/src/components/SchemaView/ScalarView.tsx
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2021 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Box, Typography } from '@material-ui/core';
+import React from 'react';
+import { MetadataView } from './MetadataView';
+import { SchemaViewProps } from './types';
+
+export function ScalarView({ schema }: SchemaViewProps) {
+ return (
+ <>
+ {schema.description && (
+
+ {schema.description}
+
+ )}
+
+ >
+ );
+}
diff --git a/plugins/config-schema/src/components/SchemaView/SchemaView.tsx b/plugins/config-schema/src/components/SchemaView/SchemaView.tsx
new file mode 100644
index 0000000000..bf5d7fd18c
--- /dev/null
+++ b/plugins/config-schema/src/components/SchemaView/SchemaView.tsx
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2021 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React from 'react';
+import { ArrayView } from './ArrayView';
+import { MatchView } from './MatchView';
+import { ObjectView } from './ObjectView';
+import { ScalarView } from './ScalarView';
+import { SchemaViewProps } from './types';
+
+export function SchemaView(props: SchemaViewProps) {
+ const { schema } = props;
+ if (schema.anyOf) {
+ return (
+
+ );
+ }
+ if (schema.oneOf) {
+ return (
+
+ );
+ }
+ if (schema.allOf) {
+ return (
+
+ );
+ }
+ switch (schema.type) {
+ case 'array':
+ return ;
+ case 'object':
+ case undefined:
+ return ;
+ default:
+ return ;
+ }
+}
diff --git a/plugins/config-schema/src/components/SchemaView/index.ts b/plugins/config-schema/src/components/SchemaView/index.ts
new file mode 100644
index 0000000000..8840696be6
--- /dev/null
+++ b/plugins/config-schema/src/components/SchemaView/index.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2021 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export { SchemaView } from './SchemaView';
diff --git a/plugins/config-schema/src/components/SchemaView/types.ts b/plugins/config-schema/src/components/SchemaView/types.ts
new file mode 100644
index 0000000000..94b676ec3e
--- /dev/null
+++ b/plugins/config-schema/src/components/SchemaView/types.ts
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2021 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Schema } from 'jsonschema';
+
+export interface SchemaViewProps {
+ path: string;
+ depth: number;
+ schema: Schema;
+}
diff --git a/plugins/config-schema/src/components/SchemaViewer/SchemaViewer.test.tsx b/plugins/config-schema/src/components/SchemaViewer/SchemaViewer.test.tsx
new file mode 100644
index 0000000000..48dc92098a
--- /dev/null
+++ b/plugins/config-schema/src/components/SchemaViewer/SchemaViewer.test.tsx
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2021 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { render, screen } from '@testing-library/react';
+import { Schema } from 'jsonschema';
+import React from 'react';
+import { SchemaViewer } from './SchemaViewer';
+
+describe('SchemaViewer', () => {
+ it('should render a simple schema', () => {
+ const schema = {
+ type: 'object',
+ properties: {
+ a: {
+ description: 'My A',
+ type: 'string',
+ },
+ b: {
+ description: 'My B',
+ type: 'number',
+ },
+ },
+ };
+
+ render();
+
+ expect(screen.getAllByText('a').length).toBe(2);
+ expect(screen.getByText('My A')).toBeInTheDocument();
+
+ expect(screen.getAllByText('b').length).toBe(2);
+ expect(screen.getByText('My B')).toBeInTheDocument();
+ });
+
+ it('should render complex schema', () => {
+ const schema: Schema = {
+ type: 'object',
+ properties: {
+ a: {
+ description: 'My A',
+ type: 'object',
+ patternProperties: {
+ 'prefix.*': {
+ description: 'Prefix prop',
+ type: 'string',
+ },
+ },
+ additionalProperties: {
+ description: 'Additional properties for A',
+ type: 'number',
+ minimum: 72,
+ maximum: 79,
+ },
+ },
+ b: {
+ oneOf: [
+ { type: 'string', description: 'B one of 1' },
+ {
+ type: 'array',
+ description: 'B one of 2',
+ items: {
+ anyOf: [
+ { type: 'string', description: 'Any of B 1' },
+ { type: 'number', description: 'Any of B 2' },
+ {
+ type: 'object',
+ description: 'Any of B 3',
+ properties: {
+ deep: {
+ allOf: [
+ {
+ type: 'number',
+ description: 'Impossible 1',
+ },
+ {
+ type: 'boolean',
+ description: 'Impossible 2',
+ },
+ ],
+ },
+ },
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ };
+
+ render();
+
+ expect(screen.getAllByText('a').length).toBe(2);
+ expect(screen.getByText('My A')).toBeInTheDocument();
+ expect(screen.getByText('a.')).toBeInTheDocument();
+ expect(screen.getByText('Prefix prop')).toBeInTheDocument();
+
+ expect(screen.getByText('a.*')).toBeInTheDocument();
+ expect(screen.getByText('Additional properties for A')).toBeInTheDocument();
+ expect(screen.getByText('72')).toBeInTheDocument();
+ expect(screen.getByText('79')).toBeInTheDocument();
+
+ expect(screen.getAllByText('b').length).toBe(2);
+ expect(screen.getByText('b/1')).toBeInTheDocument();
+ expect(screen.getByText('B one of 1')).toBeInTheDocument();
+
+ expect(screen.getByText('b/2')).toBeInTheDocument();
+ expect(screen.getByText('B one of 2')).toBeInTheDocument();
+
+ expect(screen.getByText('b/2[]')).toBeInTheDocument();
+ expect(screen.getByText('b/2[]/1')).toBeInTheDocument();
+ expect(screen.getByText('Any of B 1')).toBeInTheDocument();
+ expect(screen.getByText('b/2[]/2')).toBeInTheDocument();
+ expect(screen.getByText('Any of B 2')).toBeInTheDocument();
+ expect(screen.getByText('b/2[]/3')).toBeInTheDocument();
+ expect(screen.getByText('Any of B 3')).toBeInTheDocument();
+
+ expect(screen.getByText('b/2[]/3.deep')).toBeInTheDocument();
+ expect(screen.getByText('b/2[]/3.deep/1')).toBeInTheDocument();
+ expect(screen.getByText('Impossible 1')).toBeInTheDocument();
+ expect(screen.getByText('b/2[]/3.deep/2')).toBeInTheDocument();
+ expect(screen.getByText('Impossible 2')).toBeInTheDocument();
+ });
+});
diff --git a/plugins/config-schema/src/components/SchemaViewer/SchemaViewer.tsx b/plugins/config-schema/src/components/SchemaViewer/SchemaViewer.tsx
new file mode 100644
index 0000000000..3ed3cca0ee
--- /dev/null
+++ b/plugins/config-schema/src/components/SchemaViewer/SchemaViewer.tsx
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2021 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Box, Paper } from '@material-ui/core';
+import { Schema } from 'jsonschema';
+import React from 'react';
+import { SchemaView } from '../SchemaView';
+import { SchemaBrowser } from '../SchemaBrowser';
+import { ScrollTargetsProvider } from '../ScrollTargetsContext/ScrollTargetsContext';
+
+export interface SchemaViewerProps {
+ schema: Schema;
+}
+
+export const SchemaViewer = ({ schema }: SchemaViewerProps) => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/plugins/config-schema/src/components/SchemaViewer/index.ts b/plugins/config-schema/src/components/SchemaViewer/index.ts
new file mode 100644
index 0000000000..4852c4e21d
--- /dev/null
+++ b/plugins/config-schema/src/components/SchemaViewer/index.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2021 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export { SchemaViewer } from './SchemaViewer';
diff --git a/plugins/config-schema/src/components/ScrollTargetsContext/ScrollTargetsContext.tsx b/plugins/config-schema/src/components/ScrollTargetsContext/ScrollTargetsContext.tsx
new file mode 100644
index 0000000000..085b237754
--- /dev/null
+++ b/plugins/config-schema/src/components/ScrollTargetsContext/ScrollTargetsContext.tsx
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2021 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React, { createContext, ReactNode, useContext } from 'react';
+
+class ScrollTargetsForwarder {
+ private readonly listeners = new Map void>();
+
+ setScrollListener(id: string, listener: () => void): () => void {
+ this.listeners.set(id, listener);
+
+ return () => {
+ if (this.listeners.get(id) === listener) {
+ this.listeners.delete(id);
+ }
+ };
+ }
+
+ scrollTo(id: string) {
+ this.listeners.get(id)?.();
+ }
+}
+
+const ScrollTargetsContext = createContext(
+ undefined,
+);
+
+export function ScrollTargetsProvider({ children }: { children: ReactNode }) {
+ return (
+
+ );
+}
+
+export function useScrollTargets() {
+ return useContext(ScrollTargetsContext);
+}
diff --git a/plugins/config-schema/src/components/ScrollTargetsContext/index.ts b/plugins/config-schema/src/components/ScrollTargetsContext/index.ts
new file mode 100644
index 0000000000..d2d35ec6b1
--- /dev/null
+++ b/plugins/config-schema/src/components/ScrollTargetsContext/index.ts
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2021 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export {
+ ScrollTargetsProvider,
+ useScrollTargets,
+} from './ScrollTargetsContext';
diff --git a/plugins/config-schema/src/index.ts b/plugins/config-schema/src/index.ts
new file mode 100644
index 0000000000..009b093d50
--- /dev/null
+++ b/plugins/config-schema/src/index.ts
@@ -0,0 +1,18 @@
+/*
+ * Copyright 2021 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export * from './api';
+export { configSchemaPlugin, ConfigSchemaPage } from './plugin';
diff --git a/plugins/config-schema/src/plugin.test.ts b/plugins/config-schema/src/plugin.test.ts
new file mode 100644
index 0000000000..0f71d800d4
--- /dev/null
+++ b/plugins/config-schema/src/plugin.test.ts
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2021 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { configSchemaPlugin } from './plugin';
+
+describe('config-schema', () => {
+ it('should export plugin', () => {
+ expect(configSchemaPlugin).toBeDefined();
+ });
+});
diff --git a/plugins/config-schema/src/plugin.ts b/plugins/config-schema/src/plugin.ts
new file mode 100644
index 0000000000..841f9f7f04
--- /dev/null
+++ b/plugins/config-schema/src/plugin.ts
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2021 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { createPlugin, createRoutableExtension } from '@backstage/core';
+
+import { rootRouteRef } from './routes';
+
+export const configSchemaPlugin = createPlugin({
+ id: 'config-schema',
+ routes: {
+ root: rootRouteRef,
+ },
+});
+
+export const ConfigSchemaPage = configSchemaPlugin.provide(
+ createRoutableExtension({
+ component: () =>
+ import('./components/ConfigSchemaPage').then(m => m.ConfigSchemaPage),
+ mountPoint: rootRouteRef,
+ }),
+);
diff --git a/plugins/config-schema/src/routes.ts b/plugins/config-schema/src/routes.ts
new file mode 100644
index 0000000000..8128e1f49f
--- /dev/null
+++ b/plugins/config-schema/src/routes.ts
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2021 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { createRouteRef } from '@backstage/core';
+
+export const rootRouteRef = createRouteRef({
+ title: 'config-schema',
+});
diff --git a/plugins/config-schema/src/setupTests.ts b/plugins/config-schema/src/setupTests.ts
new file mode 100644
index 0000000000..0cec5b395d
--- /dev/null
+++ b/plugins/config-schema/src/setupTests.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2021 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import '@testing-library/jest-dom';
+import 'cross-fetch/polyfill';
diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/KubernetesAuthTranslatorGenerator.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/KubernetesAuthTranslatorGenerator.ts
index 4ae2aff35a..d222bdbce1 100644
--- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/KubernetesAuthTranslatorGenerator.ts
+++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/KubernetesAuthTranslatorGenerator.ts
@@ -21,7 +21,7 @@ import { AwsIamKubernetesAuthTranslator } from './AwsIamKubernetesAuthTranslator
export class KubernetesAuthTranslatorGenerator {
static getKubernetesAuthTranslatorInstance(
- authProvider: String,
+ authProvider: string,
): KubernetesAuthTranslator {
switch (authProvider) {
case 'google': {
diff --git a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts
index e76a114d54..7ec1a317d8 100644
--- a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts
+++ b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts
@@ -34,8 +34,8 @@ const mockFetch = (mock: jest.Mock) => {
};
function generateMockResourcesAndErrors(
- serviceId: String,
- clusterName: String,
+ serviceId: string,
+ clusterName: string,
) {
if (clusterName === 'empty-cluster') {
return {
diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.test.ts
index fb9d3aeaa6..3b17f46bb3 100644
--- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.test.ts
+++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.test.ts
@@ -16,6 +16,8 @@
import mockFs from 'mock-fs';
import { Writable } from 'stream';
+import os from 'os';
+import { resolve as resolvePath } from 'path';
import {
PullRequestCreator,
GithubPullRequestActionInput,
@@ -28,7 +30,8 @@ import { getRootLogger } from '@backstage/backend-common';
import { ScmIntegrations } from '@backstage/integration';
import { ConfigReader } from '@backstage/config';
-const id = 'createPublishGithubPullRequestAction';
+const root = os.platform() === 'win32' ? 'C:\\root' : '/root';
+const workspacePath = resolvePath(root, 'my-workspace');
describe('createPublishGithubPullRequestAction', () => {
let instance: TemplateAction;
@@ -72,7 +75,7 @@ describe('createPublishGithubPullRequestAction', () => {
};
mockFs({
- [id]: { 'file.txt': 'Hello there!' },
+ [workspacePath]: { 'file.txt': 'Hello there!' },
});
ctx = {
@@ -81,7 +84,7 @@ describe('createPublishGithubPullRequestAction', () => {
logger: getRootLogger(),
logStream: new Writable(),
input,
- workspacePath: id,
+ workspacePath,
};
});
it('creates a pull request', async () => {
@@ -133,7 +136,7 @@ describe('createPublishGithubPullRequestAction', () => {
};
mockFs({
- [id]: {
+ [workspacePath]: {
source: { 'foo.txt': 'Hello there!' },
irrelevant: { 'bar.txt': 'Nothing to see here' },
},
@@ -145,7 +148,7 @@ describe('createPublishGithubPullRequestAction', () => {
logger: getRootLogger(),
logStream: new Writable(),
input,
- workspacePath: id,
+ workspacePath,
};
});
@@ -188,7 +191,7 @@ describe('createPublishGithubPullRequestAction', () => {
};
mockFs({
- [id]: { 'file.txt': 'Hello there!' },
+ [workspacePath]: { 'file.txt': 'Hello there!' },
});
ctx = {
@@ -197,7 +200,7 @@ describe('createPublishGithubPullRequestAction', () => {
logger: getRootLogger(),
logStream: new Writable(),
input,
- workspacePath: id,
+ workspacePath,
};
});
it('creates a pull request', async () => {
diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts
index 3a00610a92..e2e4f17efb 100644
--- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts
+++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts
@@ -197,17 +197,21 @@ export const createPublishGithubPullRequestAction = ({
const client = await clientFactory({ integrations, host, owner, repo });
const fileRoot = sourcePath
- ? path.join(ctx.workspacePath, sourcePath)
+ ? path.resolve(ctx.workspacePath, sourcePath)
: ctx.workspacePath;
- const localFilePaths = await globby(`${fileRoot}/**/*.*`);
+
+ const localFilePaths = await globby(['./**', './**/.*', '!.git'], {
+ cwd: fileRoot,
+ gitignore: true,
+ dot: true,
+ });
const fileContents = await Promise.all(
- localFilePaths.map(p => readFile(p)),
+ localFilePaths.map(p => readFile(path.resolve(fileRoot, p))),
);
- const repoFilePaths = localFilePaths.map(p => {
- const relativePath = path.relative(fileRoot, p);
- return targetPath ? `${targetPath}/${relativePath}` : relativePath;
+ const repoFilePaths = localFilePaths.map(repoFilePath => {
+ return targetPath ? `${targetPath}/${repoFilePath}` : repoFilePath;
});
const changes = [
diff --git a/plugins/sonarqube/README.md b/plugins/sonarqube/README.md
index a07bb2964d..27fa81066b 100644
--- a/plugins/sonarqube/README.md
+++ b/plugins/sonarqube/README.md
@@ -67,8 +67,12 @@ proxy:
allowedMethods: ['GET']
headers:
Authorization:
- # Content: 'Basic base64(":")' <-- note the trailing ':'
- # Example: Basic bXktYXBpLWtleTo=
+ # Environmental variable: SONARQUBE_AUTH_HEADER
+ # Value: 'Basic base64(":")'
+ # Encode the ":" string using base64 encoder.
+ # Note the trailing colon (:) at the end of the token.
+ # Example environmental config: SONARQUBE_AUTH_HEADER=Basic bXktYXBpLWtleTo=
+ # Fetch the sonar-auth-token from https://sonarcloud.io/account/security/
$env: SONARQUBE_AUTH_HEADER
sonarQube:
@@ -77,7 +81,14 @@ sonarQube:
5. Get and provide `SONARQUBE_AUTH_HEADER` as env variable (https://sonarcloud.io/account/security or https://docs.sonarqube.org/latest/user-guide/user-token/)
-6. Add the `sonarqube.org/project-key` annotation to your entity's `catalog-info.yaml` file:
+6. Run the following commands in the root folder of the project to install and compile the changes.
+
+```yaml
+yarn install
+yarn tsc
+```
+
+7. Add the `sonarqube.org/project-key` annotation to the `catalog-info.yaml` file of the target repo for which code quality analysis is needed.
```yaml
apiVersion: backstage.io/v1alpha1
diff --git a/plugins/tech-radar/src/components/RadarDescription/RadarDescription.tsx b/plugins/tech-radar/src/components/RadarDescription/RadarDescription.tsx
index 20c489f630..3ac4f153be 100644
--- a/plugins/tech-radar/src/components/RadarDescription/RadarDescription.tsx
+++ b/plugins/tech-radar/src/components/RadarDescription/RadarDescription.tsx
@@ -19,6 +19,7 @@ import Dialog from '@material-ui/core/Dialog';
import DialogTitle from '@material-ui/core/DialogTitle';
import { Button, DialogActions, DialogContent } from '@material-ui/core';
import LinkIcon from '@material-ui/icons/Link';
+import { MarkdownContent } from '@backstage/core';
export type Props = {
open: boolean;
@@ -43,7 +44,9 @@ const RadarDescription = (props: Props): JSX.Element => {
{title}
- {description}
+
+
+
{url && (