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 ? ( + {`${name} + ) : ( + '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 ( + + ); +} +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 = ({