diff --git a/.changeset/cyan-seahorses-film.md b/.changeset/cyan-seahorses-film.md
new file mode 100644
index 0000000000..80a27baec0
--- /dev/null
+++ b/.changeset/cyan-seahorses-film.md
@@ -0,0 +1,11 @@
+---
+'@backstage/core-plugin-api': minor
+---
+
+**BREAKING CHANGE** The `StorageApi` has received several updates that fills in gaps for some use-cases and makes it easier to avoid mistakes:
+
+- The `StorageValueChange` type has been renamed to `StorageValueSnapshot`, the `newValue` property has been renamed to `value`, the stored value type has been narrowed to `JsonValue`, and it has received a new `presence` property that is `'unknown'`, `'absent'`, or `'present'`.
+- The `get` method has been deprecated in favor of a new `snapshot` method, which returns a `StorageValueSnapshot`.
+- The `observe$` method has had its contract changed. It should now emit values when the `presence` of a key changes, this may for example happen when remotely stored values are requested on page load and the presence switches from `'unknown'` to either `'absent'` or `'present'`.
+
+The above changes have been made with deprecations in place to maintain much of the backwards compatibility for consumers of the `StorageApi`. The only breaking change is the narrowing of the stored value type, which may in some cases require the addition of an explicit type parameter to the `get` and `observe$` methods.
diff --git a/.changeset/expor-k8s-backend-client.md b/.changeset/expor-k8s-backend-client.md
new file mode 100644
index 0000000000..b4b7bd9973
--- /dev/null
+++ b/.changeset/expor-k8s-backend-client.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-kubernetes': minor
+---
+
+Includes `KubernetesBackendClient` in the export to allow developers to use it externally.
diff --git a/.changeset/gorgeous-beers-teach.md b/.changeset/gorgeous-beers-teach.md
new file mode 100644
index 0000000000..014398a5be
--- /dev/null
+++ b/.changeset/gorgeous-beers-teach.md
@@ -0,0 +1,5 @@
+---
+'@backstage/core-app-api': minor
+---
+
+Updated `WebStorageApi` to reflect the `StorageApi` changes in `@backstage/core-plugin-api`.
diff --git a/.changeset/green-timers-film.md b/.changeset/green-timers-film.md
new file mode 100644
index 0000000000..be94b13f0d
--- /dev/null
+++ b/.changeset/green-timers-film.md
@@ -0,0 +1,5 @@
+---
+'@backstage/test-utils': minor
+---
+
+Updated `MockStorageApi` to reflect the `StorageApi` changes in `@backstage/core-plugin-api`.
diff --git a/.changeset/modern-planets-fry.md b/.changeset/modern-planets-fry.md
new file mode 100644
index 0000000000..2da39473a1
--- /dev/null
+++ b/.changeset/modern-planets-fry.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-newrelic-dashboard': patch
+---
+
+Created a New Plugin , which imports dashboard links and snapshots from New Relic
diff --git a/.changeset/moody-scissors-yell.md b/.changeset/moody-scissors-yell.md
new file mode 100644
index 0000000000..deb06b3c6f
--- /dev/null
+++ b/.changeset/moody-scissors-yell.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-home': patch
+---
+
+Fix undefined identity bug in `WelcomeTitle` caused by using deprecated methods of the IdentityApi
diff --git a/.changeset/nice-feet-hope.md b/.changeset/nice-feet-hope.md
new file mode 100644
index 0000000000..cfc660020f
--- /dev/null
+++ b/.changeset/nice-feet-hope.md
@@ -0,0 +1,30 @@
+---
+'@backstage/create-app': patch
+---
+
+The `index.html` template of the app has been updated to use the new `config` global provided by the Backstage CLI.
+
+To apply this change to an existing app, make the following changes to `packages/app/public/index.html`:
+
+```diff
+-
<%= app.title %>
++ <%= config.getString('app.title') %>
+```
+
+```diff
+- <% if (app.googleAnalyticsTrackingId && typeof app.googleAnalyticsTrackingId === 'string') { %>
++ <% if (config.has('app.googleAnalyticsTrackingId')) { %>
+
+```
+
+```diff
+- gtag('config', '<%= app.googleAnalyticsTrackingId %>');
++ gtag(
++ 'config',
++ '<%= config.getString("app.googleAnalyticsTrackingId") %>',
++ );
+```
diff --git a/.changeset/purple-grapes-kick.md b/.changeset/purple-grapes-kick.md
new file mode 100644
index 0000000000..9d64d4e17c
--- /dev/null
+++ b/.changeset/purple-grapes-kick.md
@@ -0,0 +1,15 @@
+---
+'@backstage/cli': patch
+---
+
+The frontend configuration is now available as a `config` global during templating of the `index.html` file. This allows for much more flexibility as the values available during templating is not longer hardcoded to a fixed set of values.
+
+For example, to access the app title, you would now do the following:
+
+```html
+<%= config.getString('app.title') %>
+```
+
+Along with this change, usage of the existing `app.` values has been deprecated and will be removed in a future release. The general pattern for migrating existing usage is to replace `<%= app. %>` with `<%= config.getString('app.') %>`, although in some cases you may need to use for example `config.has('app.')` or `config.getOptionalString('app.')` instead.
+
+The [`@backstage/create-app` changelog](https://github.com/backstage/backstage/blob/master/packages/create-app/CHANGELOG.md#049) also contains more details how to migrate existing usage.
diff --git a/.changeset/smooth-news-melt.md b/.changeset/smooth-news-melt.md
new file mode 100644
index 0000000000..deb745a46a
--- /dev/null
+++ b/.changeset/smooth-news-melt.md
@@ -0,0 +1,5 @@
+---
+'@backstage/core-components': patch
+---
+
+Do not `setState` when unmounted in `OverflowTooltip`
diff --git a/.changeset/soft-scissors-own.md b/.changeset/soft-scissors-own.md
new file mode 100644
index 0000000000..e57a018d50
--- /dev/null
+++ b/.changeset/soft-scissors-own.md
@@ -0,0 +1,5 @@
+---
+'embedded-techdocs-app': patch
+---
+
+The `index.html` template was updated to use the new `config` global.
diff --git a/.changeset/techdocs-sakta-vi-gar.md b/.changeset/techdocs-sakta-vi-gar.md
new file mode 100644
index 0000000000..183d90300b
--- /dev/null
+++ b/.changeset/techdocs-sakta-vi-gar.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-techdocs-backend': patch
+---
+
+Fixed a bug preventing cache from being enabled in TechDocs "recommended" deployment mode.
diff --git a/.changeset/unlucky-wolves-reply.md b/.changeset/unlucky-wolves-reply.md
new file mode 100644
index 0000000000..ac42b59abb
--- /dev/null
+++ b/.changeset/unlucky-wolves-reply.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-scaffolder': patch
+---
+
+Add DescriptionField override to support Markdown
diff --git a/app-config.yaml b/app-config.yaml
index 6cd05f44e8..2837d97d2f 100644
--- a/app-config.yaml
+++ b/app-config.yaml
@@ -74,6 +74,11 @@ proxy:
headers:
X-Api-Key: ${NEW_RELIC_REST_API_KEY}
+ '/newrelic/api':
+ target: https://api.newrelic.com
+ headers:
+ X-Api-Key: ${NEW_RELIC_USER_KEY}
+
'/pagerduty':
target: https://api.pagerduty.com
headers:
diff --git a/docs/architecture-decisions/adr000-template.md b/docs/architecture-decisions/adr000-template.md
index 5e6edd8dba..f16ae7d39f 100644
--- a/docs/architecture-decisions/adr000-template.md
+++ b/docs/architecture-decisions/adr000-template.md
@@ -1,6 +1,7 @@
---
id: adrs-adr000
-title: ADR000: [TITLE]
+title: 'ADR000: [TITLE]'
+# prettier-ignore
description: Architecture Decision Record (ADR) for [TITLE] [DESCRIPTION]
---
diff --git a/docs/architecture-decisions/adr001-add-adr-log.md b/docs/architecture-decisions/adr001-add-adr-log.md
index 872b3311a5..d1bf0f83f5 100644
--- a/docs/architecture-decisions/adr001-add-adr-log.md
+++ b/docs/architecture-decisions/adr001-add-adr-log.md
@@ -1,6 +1,7 @@
---
id: adrs-adr001
-title: ADR001: Architecture Decision Record (ADR) log
+title: 'ADR001: Architecture Decision Record (ADR) log'
+# prettier-ignore
description: Architecture Decision Record (ADR) logs as a reference point for the team
---
diff --git a/docs/architecture-decisions/adr002-default-catalog-file-format.md b/docs/architecture-decisions/adr002-default-catalog-file-format.md
index 013e2cbf39..d0a657fe46 100644
--- a/docs/architecture-decisions/adr002-default-catalog-file-format.md
+++ b/docs/architecture-decisions/adr002-default-catalog-file-format.md
@@ -1,6 +1,7 @@
---
id: adrs-adr002
-title: ADR002: Default Software Catalog File Format
+title: 'ADR002: Default Software Catalog File Format'
+# prettier-ignore
description: Architecture Decision Record (ADR) log on Default Software Catalog File Format
---
diff --git a/docs/architecture-decisions/adr003-avoid-default-exports.md b/docs/architecture-decisions/adr003-avoid-default-exports.md
index 878d0d701c..34e5cd5411 100644
--- a/docs/architecture-decisions/adr003-avoid-default-exports.md
+++ b/docs/architecture-decisions/adr003-avoid-default-exports.md
@@ -1,6 +1,7 @@
---
id: adrs-adr003
-title: ADR003: Avoid Default Exports and Prefer Named Exports
+title: 'ADR003: Avoid Default Exports and Prefer Named Exports'
+# prettier-ignore
description: Architecture Decision Record (ADR) log on Avoid Default Exports and Prefer Named Exports
---
diff --git a/docs/architecture-decisions/adr004-module-export-structure.md b/docs/architecture-decisions/adr004-module-export-structure.md
index e6e378da9d..95d0f27811 100644
--- a/docs/architecture-decisions/adr004-module-export-structure.md
+++ b/docs/architecture-decisions/adr004-module-export-structure.md
@@ -1,6 +1,7 @@
---
id: adrs-adr004
-title: ADR004: Module Export Structure
+title: 'ADR004: Module Export Structure'
+# prettier-ignore
description: Architecture Decision Record (ADR) log on Module Export Structure
---
diff --git a/docs/architecture-decisions/adr005-catalog-core-entities.md b/docs/architecture-decisions/adr005-catalog-core-entities.md
index 83196c12e2..1206d9e83d 100644
--- a/docs/architecture-decisions/adr005-catalog-core-entities.md
+++ b/docs/architecture-decisions/adr005-catalog-core-entities.md
@@ -1,6 +1,7 @@
---
id: adrs-adr005
-title: ADR005: Catalog Core Entities
+title: 'ADR005: Catalog Core Entities'
+# prettier-ignore
description: Architecture Decision Record (ADR) log on Catalog Core Entities
---
diff --git a/docs/architecture-decisions/adr006-avoid-react-fc.md b/docs/architecture-decisions/adr006-avoid-react-fc.md
index eecb4ad87a..ea43cb215d 100644
--- a/docs/architecture-decisions/adr006-avoid-react-fc.md
+++ b/docs/architecture-decisions/adr006-avoid-react-fc.md
@@ -1,6 +1,7 @@
---
id: adrs-adr006
-title: ADR006: Avoid React.FC and React.SFC
+title: 'ADR006: Avoid React.FC and React.SFC'
+# prettier-ignore
description: Architecture Decision Record (ADR) log on Avoid React.FC and React.SFC
---
diff --git a/docs/architecture-decisions/adr007-use-msw-to-mock-service-requests.md b/docs/architecture-decisions/adr007-use-msw-to-mock-service-requests.md
index 68394de17c..a61bb78787 100644
--- a/docs/architecture-decisions/adr007-use-msw-to-mock-service-requests.md
+++ b/docs/architecture-decisions/adr007-use-msw-to-mock-service-requests.md
@@ -1,6 +1,7 @@
---
id: adrs-adr007
-title: ADR007: Use MSW to mock http requests
+title: 'ADR007: Use MSW to mock http requests'
+# prettier-ignore
description: Architecture Decision Record (ADR) log on Use MSW to mock http requests
---
diff --git a/docs/architecture-decisions/adr008-default-catalog-file-name.md b/docs/architecture-decisions/adr008-default-catalog-file-name.md
index 6794dc87cc..fca65c8355 100644
--- a/docs/architecture-decisions/adr008-default-catalog-file-name.md
+++ b/docs/architecture-decisions/adr008-default-catalog-file-name.md
@@ -1,6 +1,7 @@
---
id: adrs-adr008
-title: ADR008: Default Catalog File Name
+title: 'ADR008: Default Catalog File Name'
+# prettier-ignore
description: Architecture Decision Record (ADR) log on Default Catalog File Name
---
diff --git a/docs/architecture-decisions/adr009-entity-references.md b/docs/architecture-decisions/adr009-entity-references.md
index a6fe3ee583..254ae7ac1d 100644
--- a/docs/architecture-decisions/adr009-entity-references.md
+++ b/docs/architecture-decisions/adr009-entity-references.md
@@ -1,6 +1,7 @@
---
id: adrs-adr009
-title: ADR009: Entity References
+title: 'ADR009: Entity References'
+# prettier-ignore
description: Architecture Decision Record (ADR) log on Entity References
---
diff --git a/docs/architecture-decisions/adr010-luxon-date-library.md b/docs/architecture-decisions/adr010-luxon-date-library.md
index 43f18f0f69..65cabd256c 100644
--- a/docs/architecture-decisions/adr010-luxon-date-library.md
+++ b/docs/architecture-decisions/adr010-luxon-date-library.md
@@ -1,6 +1,7 @@
---
id: adrs-adr010
-title: ADR010: Use the Luxon Date Library
+title: 'ADR010: Use the Luxon Date Library'
+# prettier-ignore
description: Architecture Decision Record (ADR) for Luxon Date Library
---
diff --git a/docs/architecture-decisions/adr011-plugin-package-structure.md b/docs/architecture-decisions/adr011-plugin-package-structure.md
index 374a3ed546..363cae01a5 100644
--- a/docs/architecture-decisions/adr011-plugin-package-structure.md
+++ b/docs/architecture-decisions/adr011-plugin-package-structure.md
@@ -1,6 +1,7 @@
---
id: adrs-adr011
-title: ADR011: Plugin Package Structure
+title: 'ADR011: Plugin Package Structure'
+# prettier-ignore
description: Architecture Decision Record (ADR) for Plugin Package Structure
---
diff --git a/docs/architecture-decisions/adr012-use-luxon-locale-and-date-presets.md b/docs/architecture-decisions/adr012-use-luxon-locale-and-date-presets.md
index 0893c105f4..25bb6c1833 100644
--- a/docs/architecture-decisions/adr012-use-luxon-locale-and-date-presets.md
+++ b/docs/architecture-decisions/adr012-use-luxon-locale-and-date-presets.md
@@ -1,6 +1,7 @@
---
id: adrs-adr012
-title: ADR012: Use Luxon.toLocaleString and date/time presets
+title: 'ADR012: Use Luxon.toLocaleString and date/time presets'
+# prettier-ignore
description: Architecture Decision Record (ADR) for using Luxon's toLocaleString method and date/time presets for displaying dates and times
---
diff --git a/docs/architecture-decisions/index.md b/docs/architecture-decisions/index.md
index f852170b1e..15d1b0e9d5 100644
--- a/docs/architecture-decisions/index.md
+++ b/docs/architecture-decisions/index.md
@@ -2,6 +2,7 @@
id: adrs-overview
title: Architecture Decision Records (ADR)
sidebar_label: Overview
+# prettier-ignore
description: Overview of Architecture Decision Records (ADR)
---
diff --git a/packages/app/package.json b/packages/app/package.json
index a9e8e8ddc8..1f609858f6 100644
--- a/packages/app/package.json
+++ b/packages/app/package.json
@@ -33,6 +33,7 @@
"@backstage/plugin-kubernetes": "^0.5.1",
"@backstage/plugin-lighthouse": "^0.2.31",
"@backstage/plugin-newrelic": "^0.3.10",
+ "@backstage/plugin-newrelic-dashboard": "^0.1.0",
"@backstage/plugin-org": "^0.3.31",
"@backstage/plugin-pagerduty": "0.3.19",
"@backstage/plugin-rollbar": "^0.3.20",
diff --git a/packages/app/public/index.html b/packages/app/public/index.html
index d86f09f0b0..8273576b01 100644
--- a/packages/app/public/index.html
+++ b/packages/app/public/index.html
@@ -47,13 +47,12 @@
min-height: 100%;
}
- <%= app.title %>
+ <%= config.getString('app.title') %>
- <% if (app.googleAnalyticsTrackingId && typeof
- app.googleAnalyticsTrackingId==='string' ) { %>
+ <% if (config.has('app.googleAnalyticsTrackingId')) { %>
- <% } %> <% if (app.datadogRum.clientToken && app.datadogRum.applicationId )
- { %>
+ <% } %> <% if (config.has('app.datadogRum')) { %>
<% } %>
diff --git a/packages/embedded-techdocs-app/public/index.html b/packages/embedded-techdocs-app/public/index.html
index a6102f010d..fb874df173 100644
--- a/packages/embedded-techdocs-app/public/index.html
+++ b/packages/embedded-techdocs-app/public/index.html
@@ -47,7 +47,7 @@
min-height: 100%;
}
- <%= app.title %>
+ <%= config.getString('app.title') %>
diff --git a/packages/test-utils/api-report.md b/packages/test-utils/api-report.md
index 24f17e0b84..a75a6d958e 100644
--- a/packages/test-utils/api-report.md
+++ b/packages/test-utils/api-report.md
@@ -12,13 +12,14 @@ import { ErrorApi } from '@backstage/core-plugin-api';
import { ErrorApiError } from '@backstage/core-plugin-api';
import { ErrorApiErrorContext } from '@backstage/core-plugin-api';
import { ExternalRouteRef } from '@backstage/core-plugin-api';
+import { JsonValue } from '@backstage/types';
import { Observable } from '@backstage/types';
import { ReactElement } from 'react';
import { ReactNode } from 'react';
import { RenderResult } from '@testing-library/react';
import { RouteRef } from '@backstage/core-plugin-api';
import { StorageApi } from '@backstage/core-plugin-api';
-import { StorageValueChange } from '@backstage/core-plugin-api';
+import { StorageValueSnapshot } from '@backstage/core-plugin-api';
// @public
export type AsyncLogCollector = () => Promise;
@@ -81,11 +82,13 @@ export class MockStorageApi implements StorageApi {
// (undocumented)
get(key: string): T | undefined;
// (undocumented)
- observe$(key: string): Observable>;
+ observe$(key: string): Observable>;
// (undocumented)
remove(key: string): Promise;
// (undocumented)
set(key: string, data: T): Promise;
+ // (undocumented)
+ snapshot(key: string): StorageValueSnapshot;
}
// @public
diff --git a/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.test.ts b/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.test.ts
index a3028f8b57..a8187cd849 100644
--- a/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.test.ts
+++ b/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.test.ts
@@ -25,6 +25,12 @@ describe('WebStorage Storage API', () => {
const storage = createMockStorage();
expect(storage.get('myfakekey')).toBeUndefined();
+ expect(storage.snapshot('myfakekey')).toEqual({
+ key: 'myfakekey',
+ presence: 'absent',
+ value: undefined,
+ newValue: undefined,
+ });
});
it('should allow the setting and getting of the simple data structures', async () => {
@@ -36,6 +42,24 @@ describe('WebStorage Storage API', () => {
expect(storage.get('myfakekey')).toBe('helloimastring');
expect(storage.get('mysecondfakekey')).toBe(1234);
expect(storage.get('mythirdfakekey')).toBe(true);
+ expect(storage.snapshot('myfakekey')).toEqual({
+ key: 'myfakekey',
+ presence: 'present',
+ value: 'helloimastring',
+ newValue: 'helloimastring',
+ });
+ expect(storage.snapshot('mysecondfakekey')).toEqual({
+ key: 'mysecondfakekey',
+ presence: 'present',
+ value: 1234,
+ newValue: 1234,
+ });
+ expect(storage.snapshot('mythirdfakekey')).toEqual({
+ key: 'mythirdfakekey',
+ presence: 'present',
+ value: true,
+ newValue: true,
+ });
});
it('should allow setting of complex datastructures', async () => {
@@ -49,6 +73,12 @@ describe('WebStorage Storage API', () => {
await storage.set('myfakekey', mockData);
expect(storage.get('myfakekey')).toEqual(mockData);
+ expect(storage.snapshot('myfakekey')).toEqual({
+ key: 'myfakekey',
+ presence: 'present',
+ value: mockData,
+ newValue: mockData,
+ });
});
it('should subscribe to key changes when setting a new value', async () => {
@@ -59,7 +89,7 @@ describe('WebStorage Storage API', () => {
const mockData = { hello: 'im a great new value' };
await new Promise(resolve => {
- storage.observe$('correctKey').subscribe({
+ storage.observe$('correctKey').subscribe({
next: (...args) => {
selectedKeyNextHandler(...args);
resolve();
@@ -75,6 +105,8 @@ describe('WebStorage Storage API', () => {
expect(selectedKeyNextHandler).toHaveBeenCalledTimes(1);
expect(selectedKeyNextHandler).toHaveBeenCalledWith({
key: 'correctKey',
+ presence: 'present',
+ value: mockData,
newValue: mockData,
});
});
@@ -105,6 +137,8 @@ describe('WebStorage Storage API', () => {
expect(selectedKeyNextHandler).toHaveBeenCalledTimes(1);
expect(selectedKeyNextHandler).toHaveBeenCalledWith({
key: 'correctKey',
+ presence: 'absent',
+ value: undefined,
newValue: undefined,
});
});
@@ -122,6 +156,21 @@ describe('WebStorage Storage API', () => {
expect(firstStorage.get(keyName)).not.toBe(secondStorage.get(keyName));
expect(firstStorage.get(keyName)).toBe('boop');
expect(secondStorage.get(keyName)).toBe('deerp');
+ expect(firstStorage.snapshot(keyName)).not.toEqual(
+ secondStorage.snapshot(keyName),
+ );
+ expect(firstStorage.snapshot(keyName)).toEqual({
+ key: keyName,
+ presence: 'present',
+ value: 'boop',
+ newValue: 'boop',
+ });
+ expect(secondStorage.snapshot(keyName)).toEqual({
+ key: keyName,
+ presence: 'present',
+ value: 'deerp',
+ newValue: 'deerp',
+ });
});
it('should not clash with other namespaces when creating buckets', async () => {
@@ -138,6 +187,9 @@ describe('WebStorage Storage API', () => {
await firstStorage.set('test2', { error: true });
expect(secondStorage.get('deep/test2')).toBe(undefined);
+ expect(secondStorage.snapshot('deep/test2')).toMatchObject({
+ presence: 'absent',
+ });
});
it('should not reuse storage instances between different rootStorages', async () => {
@@ -151,5 +203,17 @@ describe('WebStorage Storage API', () => {
expect(firstStorage.get('test2')).toBe(true);
expect(secondStorage.get('test2')).toBe(undefined);
+ expect(firstStorage.snapshot('test2')).toEqual({
+ key: 'test2',
+ presence: 'present',
+ value: true,
+ newValue: true,
+ });
+ expect(secondStorage.snapshot('test2')).toEqual({
+ key: 'test2',
+ presence: 'absent',
+ value: undefined,
+ newValue: undefined,
+ });
});
});
diff --git a/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.ts b/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.ts
index 6378a77474..f71d2602b5 100644
--- a/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.ts
+++ b/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.ts
@@ -14,8 +14,8 @@
* limitations under the License.
*/
-import { StorageApi, StorageValueChange } from '@backstage/core-plugin-api';
-import { Observable } from '@backstage/types';
+import { StorageApi, StorageValueSnapshot } from '@backstage/core-plugin-api';
+import { JsonValue, Observable } from '@backstage/types';
import ObservableImpl from 'zen-observable';
/**
@@ -62,20 +62,48 @@ export class MockStorageApi implements StorageApi {
}
get(key: string): T | undefined {
- return this.data[this.getKeyName(key)];
+ return this.snapshot(key).value as T | undefined;
+ }
+
+ snapshot(key: string): StorageValueSnapshot {
+ if (this.data.hasOwnProperty(this.getKeyName(key))) {
+ const data = this.data[this.getKeyName(key)];
+ return {
+ key,
+ presence: 'present',
+ value: data,
+ newValue: data,
+ };
+ }
+ return {
+ key,
+ presence: 'absent',
+ value: undefined,
+ newValue: undefined,
+ };
}
async set(key: string, data: T): Promise {
this.data[this.getKeyName(key)] = data;
- this.notifyChanges({ key, newValue: data });
+ this.notifyChanges({
+ key,
+ presence: 'present',
+ value: data,
+ newValue: data,
+ });
}
async remove(key: string): Promise {
delete this.data[this.getKeyName(key)];
- this.notifyChanges({ key, newValue: undefined });
+ this.notifyChanges({
+ key,
+ presence: 'absent',
+ value: undefined,
+ newValue: undefined,
+ });
}
- observe$(key: string): Observable> {
+ observe$(key: string): Observable> {
return this.observable.filter(({ key: messageKey }) => messageKey === key);
}
@@ -83,22 +111,22 @@ export class MockStorageApi implements StorageApi {
return `${this.namespace}/${encodeURIComponent(key)}`;
}
- private notifyChanges(message: StorageValueChange) {
+ private notifyChanges(message: StorageValueSnapshot) {
for (const subscription of this.subscribers) {
subscription.next(message);
}
}
private subscribers = new Set<
- ZenObservable.SubscriptionObserver
+ ZenObservable.SubscriptionObserver>
>();
- private readonly observable = new ObservableImpl(
- subscriber => {
- this.subscribers.add(subscriber);
- return () => {
- this.subscribers.delete(subscriber);
- };
- },
- );
+ private readonly observable = new ObservableImpl<
+ StorageValueSnapshot
+ >(subscriber => {
+ this.subscribers.add(subscriber);
+ return () => {
+ this.subscribers.delete(subscriber);
+ };
+ });
}
diff --git a/plugins/catalog-react/src/apis/StarredEntitiesApi/migration.ts b/plugins/catalog-react/src/apis/StarredEntitiesApi/migration.ts
index 7147d449ab..8405d9e08d 100644
--- a/plugins/catalog-react/src/apis/StarredEntitiesApi/migration.ts
+++ b/plugins/catalog-react/src/apis/StarredEntitiesApi/migration.ts
@@ -41,8 +41,7 @@ export async function performMigrationToTheNewBucket({
// nothing to do
return;
}
-
- const targetEntities = new Set(target.get('entityRefs') ?? []);
+ const targetEntities = new Set(target.get('entityRefs') ?? []);
oldStarredEntities
.filter(isString)
diff --git a/plugins/home/src/homePageComponents/WelcomeTitle/WelcomeTitle.tsx b/plugins/home/src/homePageComponents/WelcomeTitle/WelcomeTitle.tsx
index 04ec7e1673..9cb18bd430 100644
--- a/plugins/home/src/homePageComponents/WelcomeTitle/WelcomeTitle.tsx
+++ b/plugins/home/src/homePageComponents/WelcomeTitle/WelcomeTitle.tsx
@@ -13,20 +13,39 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import { identityApiRef, useApi } from '@backstage/core-plugin-api';
+import {
+ alertApiRef,
+ identityApiRef,
+ useApi,
+} from '@backstage/core-plugin-api';
import { Tooltip } from '@material-ui/core';
-import React, { useMemo } from 'react';
+import React, { useEffect, useMemo } from 'react';
+import { useAsync } from 'react-use';
import { getTimeBasedGreeting } from './timeUtil';
export const WelcomeTitle = () => {
const identityApi = useApi(identityApiRef);
- const profile = identityApi.getProfile();
- const userId = identityApi.getUserId();
+ const alertApi = useApi(alertApiRef);
const greeting = useMemo(() => getTimeBasedGreeting(), []);
+ const { value: profile, error } = useAsync(() =>
+ identityApi.getProfileInfo(),
+ );
+
+ useEffect(() => {
+ if (error) {
+ alertApi.post({
+ message: `Failed to load user identity: ${error}`,
+ severity: 'error',
+ });
+ }
+ }, [error, alertApi]);
+
return (
- {`${greeting.greeting}, ${profile.displayName || userId}!`}
+ {`${greeting.greeting}${
+ profile?.displayName ? `, ${profile?.displayName}` : ''
+ }!`}
);
};
diff --git a/plugins/kubernetes/api-report.md b/plugins/kubernetes/api-report.md
index f7afc7af9d..33ac98fb76 100644
--- a/plugins/kubernetes/api-report.md
+++ b/plugins/kubernetes/api-report.md
@@ -7,7 +7,9 @@
import { ApiRef } from '@backstage/core-plugin-api';
import { BackstagePlugin } from '@backstage/core-plugin-api';
+import { DiscoveryApi } from '@backstage/core-plugin-api';
import { Entity } from '@backstage/catalog-model';
+import { IdentityApi } from '@backstage/core-plugin-api';
import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-common';
import { OAuthApi } from '@backstage/core-plugin-api';
import { ObjectsByEntityResponse } from '@backstage/plugin-kubernetes-common';
@@ -89,6 +91,27 @@ export interface KubernetesAuthProvidersApi {
// @public (undocumented)
export const kubernetesAuthProvidersApiRef: ApiRef;
+// Warning: (ae-missing-release-tag) "KubernetesBackendClient" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
+//
+// @public (undocumented)
+export class KubernetesBackendClient implements KubernetesApi {
+ constructor(options: {
+ discoveryApi: DiscoveryApi;
+ identityApi: IdentityApi;
+ });
+ // (undocumented)
+ getClusters(): Promise<
+ {
+ name: string;
+ authProvider: string;
+ }[]
+ >;
+ // (undocumented)
+ getObjectsByEntity(
+ requestBody: KubernetesRequestBody,
+ ): Promise;
+}
+
// Warning: (ae-missing-release-tag) "kubernetesPlugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
diff --git a/plugins/kubernetes/src/api/index.ts b/plugins/kubernetes/src/api/index.ts
index 33c49db099..c21a258279 100644
--- a/plugins/kubernetes/src/api/index.ts
+++ b/plugins/kubernetes/src/api/index.ts
@@ -16,3 +16,4 @@
export { kubernetesApiRef } from './types';
export type { KubernetesApi } from './types';
+export { KubernetesBackendClient } from './KubernetesBackendClient';
diff --git a/plugins/newrelic-dashboard/.eslintrc.js b/plugins/newrelic-dashboard/.eslintrc.js
new file mode 100644
index 0000000000..13573efa9c
--- /dev/null
+++ b/plugins/newrelic-dashboard/.eslintrc.js
@@ -0,0 +1,3 @@
+module.exports = {
+ extends: [require.resolve('@backstage/cli/config/eslint')],
+};
diff --git a/plugins/newrelic-dashboard/README.md b/plugins/newrelic-dashboard/README.md
new file mode 100644
index 0000000000..a31042b3fe
--- /dev/null
+++ b/plugins/newrelic-dashboard/README.md
@@ -0,0 +1,79 @@
+# New Relic Dashboard Plugin
+
+Welcome to the newrelic-dashboard plugin!
+
+## Features
+
+- Adds New Relic Dashboard Pages Links to Overview section of the catalog
+- Shows Snapshots of dashboards in New Relic
+
+## Getting started
+
+This plugin uses the Backstage proxy to securely communicate with New Relic's APIs. We use NerdGraph (New Relic's GraphQL API)
+
+To generate a New Relic API Key , you can visit this [link](https://one.newrelic.com/launcher/api-keys-ui.api-keys-launcher)
+
+1. Add the following to your app-config.yaml to enable this configuration:
+
+```
+// app-config.yaml
+proxy:
+ '/newrelic/api':
+ target: https://api.newrelic.com
+ headers:
+ X-Api-Key: ${NEW_RELIC_USER_KEY}
+```
+
+2. Add the following to `EntityPage.tsx` to display New Relic Dashboard Tab
+
+```
+// In packages/app/src/components/catalog/EntityPage.tsx
+import {
+ isNewRelicDashboardAvailable,
+ EntityNewRelicDashboardContent,
+ EntityNewRelicDashboardCard,
+} from '@backstage/plugin-newrelic-dashboard';
+
+const serviceEntityPage = (
+
+ {/* other tabs... */}
+
+
+
+```
+
+3. Add the following in `EntityPage.tsx` to display dashboard links in overview page
+
+```
+const overviewContent = (
+ {/* other tabs... */}
+
+
+
+
+
+
+
+```
+
+4. Add `newrelic.com/dashboard-guid` annotation in catalog descriptor file
+
+To Obtain the dashboard's GUID: Click the info icon by the dashboard's name to access the See metadata and manage tags modal and see the dashboard's GUID.
+
+```
+// catalog-info.yaml
+apiVersion: backstage.io/v1alpha1
+kind: Component
+metadata:
+ # ...
+ annotations:
+ newrelic.com/dashboard-guid:
+spec:
+ type: service
+```
+
+All set , you will be able to see the plugin in action!
diff --git a/plugins/newrelic-dashboard/api-report.md b/plugins/newrelic-dashboard/api-report.md
new file mode 100644
index 0000000000..ffdd5a2833
--- /dev/null
+++ b/plugins/newrelic-dashboard/api-report.md
@@ -0,0 +1,38 @@
+## API Report File for "@backstage/plugin-newrelic-dashboard"
+
+> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
+
+```ts
+///
+
+import { BackstagePlugin } from '@backstage/core-plugin-api';
+import { Entity } from '@backstage/catalog-model';
+import { RouteRef } from '@backstage/core-plugin-api';
+
+// Warning: (ae-missing-release-tag) "EntityNewRelicDashboardCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
+//
+// @public (undocumented)
+export const EntityNewRelicDashboardCard: () => JSX.Element;
+
+// Warning: (ae-missing-release-tag) "EntityNewRelicDashboardContent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
+//
+// @public (undocumented)
+export const EntityNewRelicDashboardContent: () => JSX.Element;
+
+// Warning: (ae-missing-release-tag) "isNewRelicDashboardAvailable" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
+//
+// @public (undocumented)
+export const isNewRelicDashboardAvailable: (entity: Entity) => boolean;
+
+// Warning: (ae-missing-release-tag) "newRelicDashboardPlugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
+//
+// @public (undocumented)
+export const newRelicDashboardPlugin: BackstagePlugin<
+ {
+ root: RouteRef;
+ },
+ {}
+>;
+
+// (No @packageDocumentation comment for this package)
+```
diff --git a/plugins/newrelic-dashboard/dev/index.tsx b/plugins/newrelic-dashboard/dev/index.tsx
new file mode 100644
index 0000000000..d0db8c4d96
--- /dev/null
+++ b/plugins/newrelic-dashboard/dev/index.tsx
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2021 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { createDevApp } from '@backstage/dev-utils';
+import { newRelicDashboardPlugin } from './../src/plugin';
+
+createDevApp().registerPlugin(newRelicDashboardPlugin).render();
diff --git a/plugins/newrelic-dashboard/package.json b/plugins/newrelic-dashboard/package.json
new file mode 100644
index 0000000000..3da22d1c9e
--- /dev/null
+++ b/plugins/newrelic-dashboard/package.json
@@ -0,0 +1,46 @@
+{
+ "name": "@backstage/plugin-newrelic-dashboard",
+ "version": "0.1.0",
+ "main": "src/index.ts",
+ "types": "src/index.ts",
+ "license": "Apache-2.0",
+ "publishConfig": {
+ "access": "public",
+ "main": "dist/index.esm.js",
+ "types": "dist/index.d.ts"
+ },
+ "scripts": {
+ "build": "backstage-cli plugin:build",
+ "start": "backstage-cli plugin:serve",
+ "lint": "backstage-cli lint",
+ "test": "backstage-cli test",
+ "diff": "backstage-cli plugin:diff",
+ "prepack": "backstage-cli prepack",
+ "postpack": "backstage-cli postpack",
+ "clean": "backstage-cli clean"
+ },
+ "dependencies": {
+ "@backstage/catalog-model": "^0.9.8",
+ "@backstage/core-components": "^0.8.1",
+ "@backstage/core-plugin-api": "^0.3.1",
+ "@backstage/errors": "^0.1.5",
+ "@backstage/plugin-catalog-react": "^0.6.7",
+ "@material-ui/core": "^4.12.2",
+ "@material-ui/icons": "^4.9.1",
+ "@material-ui/lab": "4.0.0-alpha.57",
+ "react-use": "^17.2.4"
+ },
+ "devDependencies": {
+ "@backstage/cli": "^0.10.2",
+ "@backstage/dev-utils": "^0.2.14",
+ "@testing-library/jest-dom": "^5.10.1",
+ "@types/react": "^16.13.1 || ^17.0.0",
+ "cross-fetch": "^3.0.6"
+ },
+ "files": [
+ "dist"
+ ],
+ "peerDependencies": {
+ "react": "^16.13.1 || ^17.0.0"
+ }
+}
diff --git a/plugins/newrelic-dashboard/src/Router.tsx b/plugins/newrelic-dashboard/src/Router.tsx
new file mode 100644
index 0000000000..24a5a1037f
--- /dev/null
+++ b/plugins/newrelic-dashboard/src/Router.tsx
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2021 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { Entity } from '@backstage/catalog-model';
+import React from 'react';
+import { MissingAnnotationEmptyState } from '@backstage/core-components';
+import { Button } from '@material-ui/core';
+import { NewRelicDashboard } from './components/NewRelicDashboard';
+import { useEntity } from '@backstage/plugin-catalog-react';
+import { NEWRELIC_GUID_ANNOTATION } from './constants';
+
+export const isNewRelicDashboardAvailable = (entity: Entity) =>
+ Boolean(entity?.metadata?.annotations?.[NEWRELIC_GUID_ANNOTATION]);
+
+export const Router = () => {
+ const { entity } = useEntity();
+
+ if (isNewRelicDashboardAvailable(entity)) {
+ return ;
+ }
+
+ return (
+ <>
+
+
+ >
+ );
+};
diff --git a/plugins/newrelic-dashboard/src/api/NewRelicDashboardApi.ts b/plugins/newrelic-dashboard/src/api/NewRelicDashboardApi.ts
new file mode 100644
index 0000000000..48c7a44c3a
--- /dev/null
+++ b/plugins/newrelic-dashboard/src/api/NewRelicDashboardApi.ts
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2021 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { createApiRef } from '@backstage/core-plugin-api';
+import { DashboardEntity } from '../types/DashboardEntity';
+import { DashboardSnapshot } from '../types/DashboardSnapshot';
+import { SnapshotDetails } from '../types/SnapshotDetails';
+
+export interface DashboardEntitySummary {
+ getDashboardEntity: DashboardEntity;
+}
+
+export interface DashboardSnapshotSummary {
+ getDashboardSnapshot: DashboardSnapshot;
+}
+
+export interface SnapshotDetailsSummary {
+ getSnapshotDetails: SnapshotDetails[];
+}
+export const newRelicDashboardApiRef = createApiRef({
+ id: 'plugin.newrelicdashboard.service',
+});
+
+export type NewRelicDashboardApi = {
+ getDashboardEntity(guid: string): Promise;
+ getDashboardSnapshot(
+ guid: string,
+ duration: number,
+ ): Promise;
+};
diff --git a/plugins/newrelic-dashboard/src/api/NewRelicDashboardClient.ts b/plugins/newrelic-dashboard/src/api/NewRelicDashboardClient.ts
new file mode 100644
index 0000000000..b79cceeeda
--- /dev/null
+++ b/plugins/newrelic-dashboard/src/api/NewRelicDashboardClient.ts
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2021 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {
+ DashboardEntitySummary,
+ DashboardSnapshotSummary,
+ NewRelicDashboardApi,
+} from './NewRelicDashboardApi';
+import { DiscoveryApi } from '@backstage/core-plugin-api';
+import { DashboardEntity } from '../types/DashboardEntity';
+import { DashboardSnapshot } from '../types/DashboardSnapshot';
+import { getDashboardParentGuidQuery } from '../queries/getDashboardParentGuidQuery';
+import { getDashboardSnapshotQuery } from '../queries/getDashboardSnapshotQuery';
+import { ResponseError } from '@backstage/errors';
+
+export class NewRelicDashboardClient implements NewRelicDashboardApi {
+ private readonly discoveryApi: DiscoveryApi;
+ constructor({
+ discoveryApi,
+ }: {
+ discoveryApi: DiscoveryApi;
+ baseUrl?: string;
+ }) {
+ this.discoveryApi = discoveryApi;
+ }
+
+ private async callApi(
+ query: string,
+ variables: { [key in string]: string | number },
+ ): Promise {
+ const myHeaders = new Headers();
+ myHeaders.append('Content-Type', 'application/json');
+ const graphql = JSON.stringify({
+ query: query,
+ variables: variables,
+ });
+ const requestOptions: RequestInit = {
+ method: 'POST',
+ headers: myHeaders,
+ body: graphql,
+ redirect: 'follow',
+ };
+
+ const apiUrl = `${await this.discoveryApi.getBaseUrl(
+ 'proxy',
+ )}/newrelic/api/graphql`;
+ const response = await fetch(apiUrl, requestOptions);
+ if (response.status === 200) {
+ return (await response.json()) as T;
+ }
+ if (!response.ok) {
+ throw await ResponseError.fromResponse(response);
+ } else {
+ return undefined;
+ }
+ }
+
+ async getDashboardEntity(
+ guid: string,
+ ): Promise {
+ const DashboardEntityList = await this.callApi(
+ getDashboardParentGuidQuery,
+ {
+ query: `parentId ='${guid}'`,
+ },
+ );
+ return {
+ getDashboardEntity: DashboardEntityList!,
+ };
+ }
+
+ async getDashboardSnapshot(
+ guid: string,
+ duration: number,
+ ): Promise {
+ const DashboardSnapshotValue = await this.callApi(
+ getDashboardSnapshotQuery,
+ {
+ guid: guid,
+ duration: duration,
+ },
+ );
+ return {
+ getDashboardSnapshot: DashboardSnapshotValue!,
+ };
+ }
+}
diff --git a/plugins/newrelic-dashboard/src/api/index.ts b/plugins/newrelic-dashboard/src/api/index.ts
new file mode 100644
index 0000000000..9b14160bcf
--- /dev/null
+++ b/plugins/newrelic-dashboard/src/api/index.ts
@@ -0,0 +1,18 @@
+/*
+ * Copyright 2021 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export type { NewRelicDashboardApi } from './NewRelicDashboardApi';
+export { newRelicDashboardApiRef } from './NewRelicDashboardApi';
+export { NewRelicDashboardClient } from './NewRelicDashboardClient';
diff --git a/plugins/newrelic-dashboard/src/components/NewRelicDashboard/DashboardEntityList.tsx b/plugins/newrelic-dashboard/src/components/NewRelicDashboard/DashboardEntityList.tsx
new file mode 100644
index 0000000000..005bc42151
--- /dev/null
+++ b/plugins/newrelic-dashboard/src/components/NewRelicDashboard/DashboardEntityList.tsx
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2021 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import React from 'react';
+import { Box, makeStyles, Typography } from '@material-ui/core';
+import { newRelicDashboardApiRef } from '../../api';
+import { useApi } from '@backstage/core-plugin-api';
+import { useAsync } from 'react-use';
+import {
+ Progress,
+ InfoCard,
+ Link,
+ ErrorPanel,
+} from '@backstage/core-components';
+import DesktopMac from '@material-ui/icons/DesktopMac';
+import { DashboardEntitySummary } from '../../api/NewRelicDashboardApi';
+import { ResultEntity } from '../../types/DashboardEntity';
+import { useEntity } from '@backstage/plugin-catalog-react';
+import { NEWRELIC_GUID_ANNOTATION } from './../../constants';
+
+const useStyles = makeStyles({
+ svgIcon: {
+ display: 'inline-block',
+ '& svg': {
+ display: 'inline-block',
+ fontSize: 'inherit',
+ verticalAlign: 'baseline',
+ },
+ },
+});
+export const DashboardEntityList = () => {
+ const { entity } = useEntity();
+ const classes = useStyles();
+ const newRelicDashboardAPI = useApi(newRelicDashboardApiRef);
+ const { value, loading, error } = useAsync(async (): Promise<
+ DashboardEntitySummary | undefined
+ > => {
+ const dashboardObject: Promise =
+ newRelicDashboardAPI.getDashboardEntity(
+ String(entity.metadata.annotations?.[NEWRELIC_GUID_ANNOTATION]),
+ );
+ return dashboardObject;
+ }, [entity.metadata.annotations?.[NEWRELIC_GUID_ANNOTATION]]);
+ if (loading) {
+ return ;
+ }
+ if (error) {
+ return ;
+ }
+ return (
+
+ {value?.getDashboardEntity === undefined &&
+ 'Unauthorized Request , please check API Key'}
+ {value?.getDashboardEntity !== undefined &&
+ value?.getDashboardEntity?.data.actor.entitySearch.results?.entities
+ ?.length <= 0 && (
+ <>No Dashboard Pages found with the specified Dashboard GUID>
+ )}
+ {value?.getDashboardEntity?.data.actor.entitySearch.results.entities?.map(
+ (entityResult: ResultEntity) => {
+ return (
+
+
+
+
+
+
+
+ {entityResult.name}
+
+
+ );
+ },
+ )}
+
+ );
+};
diff --git a/plugins/newrelic-dashboard/src/components/NewRelicDashboard/DashboardSnapshotList/DashboardSnapshot.tsx b/plugins/newrelic-dashboard/src/components/NewRelicDashboard/DashboardSnapshotList/DashboardSnapshot.tsx
new file mode 100644
index 0000000000..95de46df2c
--- /dev/null
+++ b/plugins/newrelic-dashboard/src/components/NewRelicDashboard/DashboardSnapshotList/DashboardSnapshot.tsx
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2021 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import React from 'react';
+import { Box, Grid } from '@material-ui/core';
+import { useApi } from '@backstage/core-plugin-api';
+import { useAsync } from 'react-use';
+import {
+ InfoCard,
+ Progress,
+ ErrorPanel,
+ Link,
+} from '@backstage/core-components';
+import { newRelicDashboardApiRef } from '../../../api';
+import { DashboardSnapshotSummary } from './../../../api/NewRelicDashboardApi';
+
+type Props = {
+ guid: string;
+ name: string;
+ permalink: string;
+ duration: number;
+};
+
+export const DashboardSnapshot = ({
+ guid,
+ name,
+ permalink,
+ duration,
+}: Props) => {
+ const newRelicDashboardAPI = useApi(newRelicDashboardApiRef);
+ const { value, loading, error } = useAsync(async (): Promise<
+ DashboardSnapshotSummary | undefined
+ > => {
+ const dashboardObject: Promise =
+ newRelicDashboardAPI.getDashboardSnapshot(guid, duration);
+ return dashboardObject;
+ }, [guid, duration]);
+ if (loading) {
+ return ;
+ }
+ if (error) {
+ return ;
+ }
+ const url =
+ value?.getDashboardSnapshot?.data?.dashboardCreateSnapshotUrl?.replace(
+ /\pdf$/i,
+ 'png',
+ );
+ return (
+
+
+
+
+
+ {url ? (
+
+ ) : (
+ 'Dashboard loading... , click here to open if it didnt render correctly'
+ )}
+
+
+
+
+
+ );
+};
diff --git a/plugins/newrelic-dashboard/src/components/NewRelicDashboard/DashboardSnapshotList/DashboardSnapshotList.tsx b/plugins/newrelic-dashboard/src/components/NewRelicDashboard/DashboardSnapshotList/DashboardSnapshotList.tsx
new file mode 100644
index 0000000000..c839a43ac2
--- /dev/null
+++ b/plugins/newrelic-dashboard/src/components/NewRelicDashboard/DashboardSnapshotList/DashboardSnapshotList.tsx
@@ -0,0 +1,148 @@
+/*
+ * Copyright 2021 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import React, { useState } from 'react';
+import { Grid, Tab, Tabs, makeStyles } from '@material-ui/core';
+import { newRelicDashboardApiRef } from '../../../api';
+import { useApi } from '@backstage/core-plugin-api';
+import { useAsync } from 'react-use';
+import { Progress, ErrorPanel } from '@backstage/core-components';
+import { DashboardSnapshot } from './DashboardSnapshot';
+import { DashboardEntitySummary } from '../../../api/NewRelicDashboardApi';
+import { ResultEntity } from '../../../types/DashboardEntity';
+
+interface TabPanelProps {
+ children?: React.ReactNode;
+ index: number;
+ value1: number;
+}
+
+function TabPanel(props: TabPanelProps) {
+ const { children, value1, index, ...other } = props;
+
+ return (
+
+ {children}
+
+ );
+}
+function a11yProps(index: number) {
+ return {
+ id: `simple-tab-${index}`,
+ 'aria-controls': `simple-tabpanel-${index}`,
+ };
+}
+type Props = {
+ guid: string;
+};
+const useStyles = makeStyles(
+ theme => ({
+ tabsWrapper: {
+ gridArea: 'pageSubheader',
+ backgroundColor: theme.palette.background.paper,
+ paddingLeft: theme.spacing(3),
+ },
+ defaultTab: {
+ padding: theme.spacing(3, 3),
+ ...theme.typography.caption,
+ textTransform: 'uppercase',
+ fontWeight: 'bold',
+ color: theme.palette.text.secondary,
+ },
+ selected: {
+ color: theme.palette.text.primary,
+ },
+ tabRoot: {
+ '&:hover': {
+ backgroundColor: theme.palette.background.default,
+ color: theme.palette.text.primary,
+ },
+ },
+ }),
+ { name: 'DashboardHeaderTabs' },
+);
+export const DashboardSnapshotList = ({ guid }: Props) => {
+ const styles = useStyles();
+ const newRelicDashboardAPI = useApi(newRelicDashboardApiRef);
+ const { value, loading, error } = useAsync(async (): Promise<
+ DashboardEntitySummary | undefined
+ > => {
+ const dashboardObject: Promise =
+ newRelicDashboardAPI.getDashboardEntity(guid);
+ return dashboardObject;
+ }, [guid]);
+ const [value1, setValue1] = useState(0);
+ const handleChange = ({}: React.ChangeEvent<{}>, newValue: number) => {
+ setValue1(newValue);
+ };
+
+ if (loading) {
+ return ;
+ }
+ if (error) {
+ return ;
+ }
+ return (
+
+
+ {value?.getDashboardEntity?.data?.actor.entitySearch.results.entities?.map(
+ (Entity: ResultEntity, index: number) => {
+ return (
+
+ );
+ },
+ )}
+
+ {value?.getDashboardEntity?.data?.actor.entitySearch.results.entities?.map(
+ (Entity: ResultEntity, index: number) => {
+ return (
+
+
+
+ );
+ },
+ )}
+
+ );
+};
diff --git a/plugins/newrelic-dashboard/src/components/NewRelicDashboard/DashboardSnapshotList/index.ts b/plugins/newrelic-dashboard/src/components/NewRelicDashboard/DashboardSnapshotList/index.ts
new file mode 100644
index 0000000000..6b17582515
--- /dev/null
+++ b/plugins/newrelic-dashboard/src/components/NewRelicDashboard/DashboardSnapshotList/index.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2021 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export { DashboardSnapshot } from './DashboardSnapshot';
+export { DashboardSnapshotList } from './DashboardSnapshotList';
diff --git a/plugins/newrelic-dashboard/src/components/NewRelicDashboard/NewRelicDashboard.tsx b/plugins/newrelic-dashboard/src/components/NewRelicDashboard/NewRelicDashboard.tsx
new file mode 100644
index 0000000000..c9cc9bf611
--- /dev/null
+++ b/plugins/newrelic-dashboard/src/components/NewRelicDashboard/NewRelicDashboard.tsx
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2021 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import React from 'react';
+import { Grid } from '@material-ui/core';
+import { Page, Content } from '@backstage/core-components';
+import { DashboardEntityList } from './DashboardEntityList';
+import { DashboardSnapshotList } from './DashboardSnapshotList';
+import { useEntity } from '@backstage/plugin-catalog-react';
+import { NEWRELIC_GUID_ANNOTATION } from '../../constants';
+
+export const NewRelicDashboard = () => {
+ const { entity } = useEntity();
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/plugins/newrelic-dashboard/src/components/NewRelicDashboard/index.ts b/plugins/newrelic-dashboard/src/components/NewRelicDashboard/index.ts
new file mode 100644
index 0000000000..668d76a059
--- /dev/null
+++ b/plugins/newrelic-dashboard/src/components/NewRelicDashboard/index.ts
@@ -0,0 +1,16 @@
+/*
+ * Copyright 2021 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export { NewRelicDashboard } from './NewRelicDashboard';
diff --git a/plugins/newrelic-dashboard/src/constants.ts b/plugins/newrelic-dashboard/src/constants.ts
new file mode 100644
index 0000000000..dd0dcff2d8
--- /dev/null
+++ b/plugins/newrelic-dashboard/src/constants.ts
@@ -0,0 +1,16 @@
+/*
+ * Copyright 2021 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export const NEWRELIC_GUID_ANNOTATION = 'newrelic.com/dashboard-guid';
diff --git a/plugins/newrelic-dashboard/src/index.ts b/plugins/newrelic-dashboard/src/index.ts
new file mode 100644
index 0000000000..821ccd4bda
--- /dev/null
+++ b/plugins/newrelic-dashboard/src/index.ts
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2021 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export {
+ newRelicDashboardPlugin,
+ EntityNewRelicDashboardCard,
+ EntityNewRelicDashboardContent,
+} from './plugin';
+export { isNewRelicDashboardAvailable } from './Router';
diff --git a/plugins/newrelic-dashboard/src/plugin.test.ts b/plugins/newrelic-dashboard/src/plugin.test.ts
new file mode 100644
index 0000000000..e5eccd930b
--- /dev/null
+++ b/plugins/newrelic-dashboard/src/plugin.test.ts
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2021 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { newRelicDashboardPlugin } from './plugin';
+
+describe('new-relic-dashboard', () => {
+ it('should export plugin', () => {
+ expect(newRelicDashboardPlugin).toBeDefined();
+ });
+});
diff --git a/plugins/newrelic-dashboard/src/plugin.ts b/plugins/newrelic-dashboard/src/plugin.ts
new file mode 100644
index 0000000000..18397902f7
--- /dev/null
+++ b/plugins/newrelic-dashboard/src/plugin.ts
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2021 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {
+ createPlugin,
+ configApiRef,
+ createApiFactory,
+ discoveryApiRef,
+ createComponentExtension,
+} from '@backstage/core-plugin-api';
+import { newRelicDashboardApiRef, NewRelicDashboardClient } from './api';
+import { rootRouteRef } from './routes';
+
+export const newRelicDashboardPlugin = createPlugin({
+ id: 'new-relic-dashboard',
+ routes: {
+ root: rootRouteRef,
+ },
+ apis: [
+ createApiFactory({
+ api: newRelicDashboardApiRef,
+ deps: { configApi: configApiRef, discoveryApi: discoveryApiRef },
+ factory: ({ configApi, discoveryApi }) =>
+ new NewRelicDashboardClient({
+ discoveryApi,
+ baseUrl: configApi.getOptionalString('newrelicdashboard.baseUrl'),
+ }),
+ }),
+ ],
+});
+export const EntityNewRelicDashboardContent = newRelicDashboardPlugin.provide(
+ createComponentExtension({
+ name: 'EntityNewRelicDashboardPage',
+ component: {
+ lazy: () => import('./Router').then(m => m.Router),
+ },
+ }),
+);
+
+export const EntityNewRelicDashboardCard = newRelicDashboardPlugin.provide(
+ createComponentExtension({
+ name: 'EntityNewRelicDashboardListComponent',
+ component: {
+ lazy: () =>
+ import('./components/NewRelicDashboard/DashboardEntityList').then(
+ m => m.DashboardEntityList,
+ ),
+ },
+ }),
+);
diff --git a/plugins/newrelic-dashboard/src/queries/getDashboardParentGuidQuery.ts b/plugins/newrelic-dashboard/src/queries/getDashboardParentGuidQuery.ts
new file mode 100644
index 0000000000..3a06384539
--- /dev/null
+++ b/plugins/newrelic-dashboard/src/queries/getDashboardParentGuidQuery.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2021 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export const getDashboardParentGuidQuery =
+ 'query ($query: String) {\n actor {\n entitySearch(query: $query) {\n results {\n entities {\n name\n ... on DashboardEntityOutline {\n name\n guid\n }\n permalink\n }\n }\n }\n }\n}\n';
diff --git a/plugins/newrelic-dashboard/src/queries/getDashboardSnapshotQuery.ts b/plugins/newrelic-dashboard/src/queries/getDashboardSnapshotQuery.ts
new file mode 100644
index 0000000000..be683ecd7b
--- /dev/null
+++ b/plugins/newrelic-dashboard/src/queries/getDashboardSnapshotQuery.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2021 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export const getDashboardSnapshotQuery =
+ 'mutation($guid: EntityGuid! , ,$duration: Milliseconds) {\n dashboardCreateSnapshotUrl(guid: $guid , params: {timeWindow: {duration: $duration}})\n}';
diff --git a/plugins/newrelic-dashboard/src/routes.ts b/plugins/newrelic-dashboard/src/routes.ts
new file mode 100644
index 0000000000..5bc8b24457
--- /dev/null
+++ b/plugins/newrelic-dashboard/src/routes.ts
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2021 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { createRouteRef } from '@backstage/core-plugin-api';
+
+export const rootRouteRef = createRouteRef({
+ id: 'new-relic-dashboard',
+});
diff --git a/plugins/newrelic-dashboard/src/setupTests.ts b/plugins/newrelic-dashboard/src/setupTests.ts
new file mode 100644
index 0000000000..fc6dbd98f8
--- /dev/null
+++ b/plugins/newrelic-dashboard/src/setupTests.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2021 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import '@testing-library/jest-dom';
+import 'cross-fetch/polyfill';
diff --git a/plugins/newrelic-dashboard/src/types/DashboardEntity.ts b/plugins/newrelic-dashboard/src/types/DashboardEntity.ts
new file mode 100644
index 0000000000..78a1b7913d
--- /dev/null
+++ b/plugins/newrelic-dashboard/src/types/DashboardEntity.ts
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2021 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export type DashboardEntity = {
+ data: {
+ actor: {
+ entitySearch: {
+ results: {
+ entities: Array;
+ };
+ };
+ };
+ };
+};
+
+export type ResultEntity = {
+ guid: string;
+ permalink: string;
+ name: string;
+};
diff --git a/plugins/newrelic-dashboard/src/types/DashboardSnapshot.ts b/plugins/newrelic-dashboard/src/types/DashboardSnapshot.ts
new file mode 100644
index 0000000000..6c11c6f6d7
--- /dev/null
+++ b/plugins/newrelic-dashboard/src/types/DashboardSnapshot.ts
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2021 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export type DashboardSnapshot = {
+ data: {
+ dashboardCreateSnapshotUrl: string;
+ };
+};
diff --git a/plugins/newrelic-dashboard/src/types/SnapshotDetails.ts b/plugins/newrelic-dashboard/src/types/SnapshotDetails.ts
new file mode 100644
index 0000000000..e189ac52c4
--- /dev/null
+++ b/plugins/newrelic-dashboard/src/types/SnapshotDetails.ts
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2021 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export type SnapshotDetails = {
+ dashboardSnapshotUrl: string;
+ permalink: string;
+};
diff --git a/plugins/scaffolder/src/components/MultistepJsonForm/FieldOverrides/DescriptionField.tsx b/plugins/scaffolder/src/components/MultistepJsonForm/FieldOverrides/DescriptionField.tsx
new file mode 100644
index 0000000000..6d116cc52b
--- /dev/null
+++ b/plugins/scaffolder/src/components/MultistepJsonForm/FieldOverrides/DescriptionField.tsx
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2021 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React from 'react';
+import { MarkdownContent } from '@backstage/core-components';
+import { FieldProps } from '@rjsf/core';
+
+export const DescriptionField = ({ description }: FieldProps) =>
+ description && ;
diff --git a/plugins/scaffolder/src/components/MultistepJsonForm/FieldOverrides/index.ts b/plugins/scaffolder/src/components/MultistepJsonForm/FieldOverrides/index.ts
new file mode 100644
index 0000000000..d6d0bf3499
--- /dev/null
+++ b/plugins/scaffolder/src/components/MultistepJsonForm/FieldOverrides/index.ts
@@ -0,0 +1,16 @@
+/*
+ * Copyright 2021 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export { DescriptionField } from './DescriptionField';
diff --git a/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx b/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx
index 738e1f894a..a9c8eaec1d 100644
--- a/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx
+++ b/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx
@@ -30,6 +30,7 @@ import { Theme as MuiTheme } from '@rjsf/material-ui';
import React, { useState } from 'react';
import { transformSchemaToProps } from './schema';
import { Content, StructuredMetadataTable } from '@backstage/core-components';
+import * as fieldOverrides from './FieldOverrides';
const Form = withTheme(MuiTheme);
type Step = {
@@ -152,7 +153,7 @@ export const MultistepJsonForm = ({