Merge branch 'backstage:master' into core-components-docs-update

This commit is contained in:
Łukasz Żeromski
2021-12-21 17:18:15 +01:00
committed by GitHub
77 changed files with 1709 additions and 134 deletions
+11
View File
@@ -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.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-kubernetes': minor
---
Includes `KubernetesBackendClient` in the export to allow developers to use it externally.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/core-app-api': minor
---
Updated `WebStorageApi` to reflect the `StorageApi` changes in `@backstage/core-plugin-api`.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/test-utils': minor
---
Updated `MockStorageApi` to reflect the `StorageApi` changes in `@backstage/core-plugin-api`.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-newrelic-dashboard': patch
---
Created a New Plugin , which imports dashboard links and snapshots from New Relic
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-home': patch
---
Fix undefined identity bug in `WelcomeTitle` caused by using deprecated methods of the IdentityApi
+30
View File
@@ -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
- <title><%= app.title %></title>
+ <title><%= config.getString('app.title') %></title>
```
```diff
- <% if (app.googleAnalyticsTrackingId && typeof app.googleAnalyticsTrackingId === 'string') { %>
+ <% if (config.has('app.googleAnalyticsTrackingId')) { %>
<script
async
- src="https://www.googletagmanager.com/gtag/js?id=<%= app.googleAnalyticsTrackingId %>"
+ src="https://www.googletagmanager.com/gtag/js?id=<%= config.getString('app.googleAnalyticsTrackingId') %>"
></script>
```
```diff
- gtag('config', '<%= app.googleAnalyticsTrackingId %>');
+ gtag(
+ 'config',
+ '<%= config.getString("app.googleAnalyticsTrackingId") %>',
+ );
```
+15
View File
@@ -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
<title><%= config.getString('app.title') %></title>
```
Along with this change, usage of the existing `app.<key>` values has been deprecated and will be removed in a future release. The general pattern for migrating existing usage is to replace `<%= app.<key> %>` with `<%= config.getString('app.<key>') %>`, although in some cases you may need to use for example `config.has('app.<key>')` or `config.getOptionalString('app.<key>')` 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.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/core-components': patch
---
Do not `setState` when unmounted in `OverflowTooltip`
+5
View File
@@ -0,0 +1,5 @@
---
'embedded-techdocs-app': patch
---
The `index.html` template was updated to use the new `config` global.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-techdocs-backend': patch
---
Fixed a bug preventing cache from being enabled in TechDocs "recommended" deployment mode.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder': patch
---
Add DescriptionField override to support Markdown
+5
View File
@@ -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:
@@ -1,6 +1,7 @@
---
id: adrs-adr000
title: ADR000: [TITLE]
title: 'ADR000: [TITLE]'
# prettier-ignore
description: Architecture Decision Record (ADR) for [TITLE] [DESCRIPTION]
---
@@ -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
---
@@ -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
---
@@ -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
---
@@ -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
---
@@ -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
---
@@ -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
---
@@ -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
---
@@ -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
---
@@ -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
---
@@ -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
---
@@ -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
---
@@ -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
---
+1
View File
@@ -2,6 +2,7 @@
id: adrs-overview
title: Architecture Decision Records (ADR)
sidebar_label: Overview
# prettier-ignore
description: Overview of Architecture Decision Records (ADR)
---
+1
View File
@@ -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",
+13 -11
View File
@@ -47,13 +47,12 @@
min-height: 100%;
}
</style>
<title><%= app.title %></title>
<title><%= config.getString('app.title') %></title>
<% if (app.googleAnalyticsTrackingId && typeof
app.googleAnalyticsTrackingId==='string' ) { %>
<% if (config.has('app.googleAnalyticsTrackingId')) { %>
<script
async
src="https://www.googletagmanager.com/gtag/js?id=<%= app.googleAnalyticsTrackingId %>"
src="https://www.googletagmanager.com/gtag/js?id=<%= config.getString('app.googleAnalyticsTrackingId') %>"
></script>
<script>
window.dataLayer = window.dataLayer || [];
@@ -62,10 +61,12 @@
}
gtag('js', new Date());
gtag('config', '<%= app.googleAnalyticsTrackingId %>');
gtag(
'config',
'<%= config.getString("app.googleAnalyticsTrackingId") %>',
);
</script>
<% } %> <% if (app.datadogRum.clientToken && app.datadogRum.applicationId )
{ %>
<% } %> <% if (config.has('app.datadogRum')) { %>
<script>
(function (h, o, u, n, d) {
h = h[d] = h[d] || {
@@ -88,11 +89,12 @@
);
DD_RUM.onReady(function () {
DD_RUM.init({
clientToken: '<%= app.datadogRum.clientToken %>',
applicationId: '<%= app.datadogRum.applicationId %>',
site: '<%= app.datadogRum.site %>' || 'datadoghq.com',
clientToken: '<%= config.getString("app.datadogRum.clientToken") %>',
applicationId:
'<%= config.getString("app.datadogRum.applicationId") %>',
site: '<%= config.getOptionalString("app.datadogRum.site") || "datadoghq.com" %>',
service: 'backstage',
env: '<%= app.datadogRum.env %>',
env: '<%= config.getString("app.datadogRum.env") %>',
sampleRate: 100,
trackInteractions: true,
});
@@ -124,6 +124,12 @@ import {
EntityTravisCIOverviewCard,
isTravisciAvailable,
} from '@roadiehq/backstage-plugin-travis-ci';
import {
isNewRelicDashboardAvailable,
EntityNewRelicDashboardContent,
EntityNewRelicDashboardCard,
} from '@backstage/plugin-newrelic-dashboard';
import React, { ReactNode, useMemo, useState } from 'react';
const customEntityFilterKind = ['Component', 'API', 'System'];
@@ -291,6 +297,14 @@ const overviewContent = (
</EntitySwitch.Case>
</EntitySwitch>
<EntitySwitch>
<EntitySwitch.Case if={isNewRelicDashboardAvailable}>
<Grid item md={6} xs={12}>
<EntityNewRelicDashboardCard />
</Grid>
</EntitySwitch.Case>
</EntitySwitch>
<Grid item md={4} xs={12}>
<EntityLinksCard />
</Grid>
@@ -371,6 +385,14 @@ const serviceEntityPage = (
<EntityTechdocsContent />
</EntityLayout.Route>
<EntityLayout.Route
if={isNewRelicDashboardAvailable}
path="/newrelic-dashboard"
title="New Relic Dashboard"
>
<EntityNewRelicDashboardContent />
</EntityLayout.Route>
<EntityLayout.Route path="/kubernetes" title="Kubernetes">
<EntityKubernetesContent />
</EntityLayout.Route>
@@ -429,6 +451,13 @@ const websiteEntityPage = (
<EntityLayout.Route path="/docs" title="Docs">
<EntityTechdocsContent />
</EntityLayout.Route>
<EntityLayout.Route
if={isNewRelicDashboardAvailable}
path="/newrelic-dashboard"
title="New Relic Dashboard"
>
<EntityNewRelicDashboardContent />
</EntityLayout.Route>
<EntityLayout.Route path="/kubernetes" title="Kubernetes">
<EntityKubernetesContent />
+26 -15
View File
@@ -15,6 +15,7 @@
*/
import fs from 'fs-extra';
import chalk from 'chalk';
import { resolve as resolvePath } from 'path';
import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin';
import HtmlWebpackPlugin from 'html-webpack-plugin';
@@ -130,28 +131,38 @@ export async function createConfig(
}),
);
const deprecatedAppConfig = {
title: frontendConfig.getString('app.title'),
baseUrl: validBaseUrl.href,
googleAnalyticsTrackingId: frontendConfig.getOptionalString(
'app.googleAnalyticsTrackingId',
),
datadogRum: {
env: frontendConfig.getOptionalString('app.datadogRum.env'),
clientToken: frontendConfig.getOptionalString(
'app.datadogRum.clientToken',
),
applicationId: frontendConfig.getOptionalString(
'app.datadogRum.applicationId',
),
site: frontendConfig.getOptionalString('app.datadogRum.site'),
},
};
plugins.push(
new HtmlWebpackPlugin({
template: paths.targetHtml,
templateParameters: {
publicPath: validBaseUrl.pathname.replace(/\/$/, ''),
app: {
title: frontendConfig.getString('app.title'),
baseUrl: validBaseUrl.href,
googleAnalyticsTrackingId: frontendConfig.getOptionalString(
'app.googleAnalyticsTrackingId',
),
datadogRum: {
env: frontendConfig.getOptionalString('app.datadogRum.env'),
clientToken: frontendConfig.getOptionalString(
'app.datadogRum.clientToken',
get app() {
console.warn(
chalk.red(
'DEPRECATION WARNING: using `app.<key>` in the index.html template is deprecated, use `config.getString("app.<key>")` instead.',
),
applicationId: frontendConfig.getOptionalString(
'app.datadogRum.applicationId',
),
site: frontendConfig.getOptionalString('app.datadogRum.site'),
},
);
return deprecatedAppConfig;
},
config: frontendConfig,
},
}),
);
+5 -2
View File
@@ -39,6 +39,7 @@ import { gitlabAuthApiRef } from '@backstage/core-plugin-api';
import { googleAuthApiRef } from '@backstage/core-plugin-api';
import { IconComponent } from '@backstage/core-plugin-api';
import { IdentityApi } from '@backstage/core-plugin-api';
import { JsonValue } from '@backstage/types';
import { microsoftAuthApiRef } from '@backstage/core-plugin-api';
import { OAuthApi } from '@backstage/core-plugin-api';
import { OAuthRequestApi } from '@backstage/core-plugin-api';
@@ -59,7 +60,7 @@ import { RouteRef } from '@backstage/core-plugin-api';
import { SessionApi } from '@backstage/core-plugin-api';
import { SessionState } 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';
import { SubRouteRef } from '@backstage/core-plugin-api';
// @public
@@ -606,10 +607,12 @@ export class WebStorage implements StorageApi {
// (undocumented)
get<T>(key: string): T | undefined;
// (undocumented)
observe$<T>(key: string): Observable<StorageValueChange<T>>;
observe$<T>(key: string): Observable<StorageValueSnapshot<T>>;
// (undocumented)
remove(key: string): Promise<void>;
// (undocumented)
set<T>(key: string, data: T): Promise<void>;
// (undocumented)
snapshot<T extends JsonValue>(key: string): StorageValueSnapshot<T>;
}
```
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { WebStorage } from './WebStorage';
import { ErrorApi, StorageApi } from '@backstage/core-plugin-api';
@@ -33,6 +34,12 @@ describe('WebStorage Storage API', () => {
const storage = createWebStorage();
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 () => {
@@ -44,9 +51,27 @@ 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 () => {
it('should allow setting of complex data structures', async () => {
const storage = createWebStorage();
const mockData = {
@@ -57,6 +82,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 () => {
@@ -67,10 +98,12 @@ describe('WebStorage Storage API', () => {
const mockData = { hello: 'im a great new value' };
await new Promise<void>(resolve => {
storage.observe$<String>('correctKey').subscribe({
next: (...args) => {
selectedKeyNextHandler(...args);
resolve();
storage.observe$<typeof mockData>('correctKey').subscribe({
next: snapshot => {
selectedKeyNextHandler(snapshot);
if (snapshot.presence === 'present') {
resolve();
}
},
});
@@ -79,10 +112,12 @@ describe('WebStorage Storage API', () => {
storage.set('correctKey', mockData);
});
expect(wrongKeyNextHandler).not.toHaveBeenCalled();
expect(wrongKeyNextHandler).toHaveBeenCalledTimes(0);
expect(selectedKeyNextHandler).toHaveBeenCalledTimes(1);
expect(selectedKeyNextHandler).toHaveBeenCalledWith({
key: 'correctKey',
presence: 'present',
value: mockData,
newValue: mockData,
});
});
@@ -98,9 +133,11 @@ describe('WebStorage Storage API', () => {
await new Promise<void>(resolve => {
storage.observe$('correctKey').subscribe({
next: (...args) => {
selectedKeyNextHandler(...args);
resolve();
next: snapshot => {
selectedKeyNextHandler(snapshot);
if (snapshot.presence === 'absent') {
resolve();
}
},
});
@@ -109,10 +146,12 @@ describe('WebStorage Storage API', () => {
storage.remove('correctKey');
});
expect(wrongKeyNextHandler).not.toHaveBeenCalled();
expect(wrongKeyNextHandler).toHaveBeenCalledTimes(0);
expect(selectedKeyNextHandler).toHaveBeenCalledTimes(1);
expect(selectedKeyNextHandler).toHaveBeenCalledWith({
key: 'correctKey',
presence: 'absent',
value: undefined,
newValue: undefined,
});
});
@@ -130,9 +169,24 @@ 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 namesapces when creating buckets', async () => {
it('should not clash with other namespaces when creating buckets', async () => {
const rootStorage = createWebStorage();
// when getting key test2 it will translate to /profile/something/deep/test2
@@ -145,7 +199,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 call the error api when the json can not be parsed in local storage', async () => {
@@ -155,9 +211,14 @@ describe('WebStorage Storage API', () => {
localStorage.setItem('/Test/Mock/Thing/key', '{smd: asdouindA}');
const value = rootStorage.get('key');
const value = rootStorage.snapshot('key');
expect(value).toBe(undefined);
expect(value).toEqual({
key: 'key',
presence: 'absent',
value: undefined,
newValue: undefined,
});
expect(mockErrorApi.post).toHaveBeenCalledWith(expect.any(Error));
expect(mockErrorApi.post).toHaveBeenCalledWith(
expect.objectContaining({
@@ -166,11 +227,35 @@ describe('WebStorage Storage API', () => {
);
});
it('should return a singleton for the same namespace and same bucket', async () => {
it('should return a stable reference for the same namespace and same bucket', async () => {
const rootStorage = createWebStorage({
namespace: '/Test/Mock/Thing/Thing ',
});
expect(rootStorage.forBucket('test')).toBe(rootStorage.forBucket('test'));
});
it('should freeze the snapshot value', async () => {
const storage = createWebStorage();
const data = { foo: 'bar', baz: [{ foo: 'bar' }] };
storage.set('foo', data);
const snapshot = storage.snapshot<typeof data>('foo');
expect(snapshot.value).not.toBe(data);
if (snapshot.presence !== 'present') {
throw new Error('Invalid presence');
}
expect(() => {
snapshot.value.foo = 'buzz';
}).toThrow(/Cannot assign to read only property/);
expect(() => {
snapshot.value.baz[0].foo = 'buzz';
}).toThrow(/Cannot assign to read only property/);
expect(() => {
snapshot.value.baz.push({ foo: 'buzz' });
}).toThrow(/Cannot add property 1, object is not extensible/);
});
});
@@ -16,10 +16,10 @@
import {
StorageApi,
StorageValueChange,
StorageValueSnapshot,
ErrorApi,
} from '@backstage/core-plugin-api';
import { Observable } from '@backstage/types';
import { JsonValue, Observable } from '@backstage/types';
import ObservableImpl from 'zen-observable';
const buckets = new Map<string, WebStorage>();
@@ -43,16 +43,29 @@ export class WebStorage implements StorageApi {
}
get<T>(key: string): T | undefined {
return this.snapshot(key).value as T | undefined;
}
snapshot<T extends JsonValue>(key: string): StorageValueSnapshot<T> {
let value = undefined;
let presence: 'present' | 'absent' = 'absent';
try {
const storage = JSON.parse(localStorage.getItem(this.getKeyName(key))!);
return storage ?? undefined;
const item = localStorage.getItem(this.getKeyName(key));
if (item) {
value = JSON.parse(item, (_key, val) => {
if (typeof val === 'object' && val !== null) {
Object.freeze(val);
}
return val;
});
presence = 'present';
}
} catch (e) {
this.errorApi.post(
new Error(`Error when parsing JSON config from storage for: ${key}`),
);
}
return undefined;
return { key, value, newValue: value, presence };
}
forBucket(name: string): WebStorage {
@@ -64,16 +77,16 @@ export class WebStorage implements StorageApi {
}
async set<T>(key: string, data: T): Promise<void> {
localStorage.setItem(this.getKeyName(key), JSON.stringify(data, null, 2));
this.notifyChanges({ key, newValue: data });
localStorage.setItem(this.getKeyName(key), JSON.stringify(data));
this.notifyChanges(key);
}
async remove(key: string): Promise<void> {
localStorage.removeItem(this.getKeyName(key));
this.notifyChanges({ key, newValue: undefined });
this.notifyChanges(key);
}
observe$<T>(key: string): Observable<StorageValueChange<T>> {
observe$<T>(key: string): Observable<StorageValueSnapshot<T>> {
return this.observable.filter(({ key: messageKey }) => messageKey === key);
}
@@ -81,22 +94,23 @@ export class WebStorage implements StorageApi {
return `${this.namespace}/${encodeURIComponent(key)}`;
}
private notifyChanges<T>(message: StorageValueChange<T>) {
private notifyChanges(key: string) {
const snapshot = this.snapshot(key);
for (const subscription of this.subscribers) {
subscription.next(message);
subscription.next(snapshot);
}
}
private subscribers = new Set<
ZenObservable.SubscriptionObserver<StorageValueChange>
ZenObservable.SubscriptionObserver<StorageValueSnapshot<JsonValue>>
>();
private readonly observable = new ObservableImpl<StorageValueChange>(
subscriber => {
this.subscribers.add(subscriber);
return () => {
this.subscribers.delete(subscriber);
};
},
);
private readonly observable = new ObservableImpl<
StorageValueSnapshot<JsonValue>
>(subscriber => {
this.subscribers.add(subscriber);
return () => {
this.subscribers.delete(subscriber);
};
});
}
@@ -18,6 +18,7 @@ import { makeStyles } from '@material-ui/core/styles';
import Tooltip, { TooltipProps } from '@material-ui/core/Tooltip';
import React, { useState } from 'react';
import TextTruncate, { TextTruncateProps } from 'react-text-truncate';
import { useMountedState } from 'react-use';
type Props = {
text: TextTruncateProps['text'];
@@ -40,10 +41,13 @@ const useStyles = makeStyles(
export function OverflowTooltip(props: Props) {
const [hover, setHover] = useState(false);
const isMounted = useMountedState();
const classes = useStyles();
const handleToggled = (truncated: boolean) => {
setHover(truncated);
if (isMounted()) {
setHover(truncated);
}
};
return (
+25 -7
View File
@@ -10,6 +10,7 @@ import { ComponentType } from 'react';
import { Config } from '@backstage/config';
import { IconComponent as IconComponent_2 } from '@backstage/core-plugin-api';
import { IdentityApi as IdentityApi_2 } from '@backstage/core-plugin-api';
import { JsonValue } from '@backstage/types';
import { Observable } from '@backstage/types';
import { ProfileInfo as ProfileInfo_2 } from '@backstage/core-plugin-api';
import { default as React_2 } from 'react';
@@ -762,20 +763,37 @@ export type SignInResult = {
// @public
export interface StorageApi {
forBucket(name: string): StorageApi;
get<T>(key: string): T | undefined;
observe$<T>(key: string): Observable<StorageValueChange<T>>;
// @deprecated
get<T extends JsonValue>(key: string): T | undefined;
observe$<T extends JsonValue>(
key: string,
): Observable<StorageValueSnapshot<T>>;
remove(key: string): Promise<void>;
set(key: string, data: any): Promise<void>;
set<T extends JsonValue>(key: string, data: T): Promise<void>;
snapshot<T extends JsonValue>(key: string): StorageValueSnapshot<T>;
}
// @public
export const storageApiRef: ApiRef<StorageApi>;
// @public @deprecated (undocumented)
export type StorageValueChange<TValue extends JsonValue> =
StorageValueSnapshot<TValue>;
// @public
export type StorageValueChange<T = any> = {
key: string;
newValue?: T;
};
export type StorageValueSnapshot<TValue extends JsonValue> =
| {
key: string;
presence: 'unknown' | 'absent';
value?: undefined;
newValue?: undefined;
}
| {
key: string;
presence: 'present';
value: TValue;
newValue?: TValue;
};
// @public
export type SubRouteRef<Params extends AnyParams = any> = {
@@ -27,8 +27,12 @@ export type FetchApi = {
};
/**
* A wrapper for the fetch API, that has additional behaviors such as the
* ability to automatically inject auth information where necessary.
* The {@link ApiRef} of {@link FetchApi}.
*
* @remarks
*
* This is a wrapper for the fetch API, that has additional behaviors such as
* the ability to automatically inject auth information where necessary.
*
* @public
*/
@@ -15,55 +15,108 @@
*/
import { ApiRef, createApiRef } from '../system';
import { Observable } from '@backstage/types';
import { JsonValue, Observable } from '@backstage/types';
/**
* Describes a value change event.
* A snapshot in time of the current known value of a storage key.
*
* @public
*/
export type StorageValueChange<T = any> = {
key: string;
newValue?: T;
};
export type StorageValueSnapshot<TValue extends JsonValue> =
| {
key: string;
presence: 'unknown' | 'absent';
value?: undefined;
/** @deprecated Use `value` instead */
newValue?: undefined;
}
| {
key: string;
presence: 'present';
value: TValue;
/** @deprecated Use `value` instead */
newValue?: TValue;
};
/**
* Provides key-value persistence API.
* @public
* @deprecated Use StorageValueSnapshot instead
*/
export type StorageValueChange<TValue extends JsonValue> =
StorageValueSnapshot<TValue>;
/**
* Provides a key-value persistence API.
*
* @public
*/
export interface StorageApi {
/**
* Create a bucket to store data in.
*
* @param name - Namespace for the storage to be stored under,
* will inherit previous namespaces too
* will inherit previous namespaces too
*/
forBucket(name: string): StorageApi;
/**
* Get the current value for persistent data, use observe$ to be notified of updates.
*
* @deprecated Use `snapshot` instead.
* @param key - Unique key associated with the data.
*/
get<T>(key: string): T | undefined;
get<T extends JsonValue>(key: string): T | undefined;
/**
* Remove persistent data.
*
* @param key - Unique key associated with the data.
*/
remove(key: string): Promise<void>;
/**
* Save persistent data, and emit messages to anyone that is using observe$ for this key
* Save persistent data, and emit messages to anyone that is using
* {@link StorageApi.observe$} for this key.
*
* @param key - Unique key associated with the data.
* @param data - The data to be stored under the key.
*/
set(key: string, data: any): Promise<void>;
set<T extends JsonValue>(key: string, data: T): Promise<void>;
/**
* Observe changes on a particular key in the bucket
* Observe the value over time for a particular key in the current bucket.
*
* @remarks
*
* The observable will only emit values when the value changes in the underlying
* storage, although multiple values with the same shape may be emitted in a row.
*
* If a {@link StorageApi.snapshot} of a key is retrieved and the presence is
* `'unknown'`, then you are guaranteed to receive a snapshot with a known
* presence, as long as you observe the key within the same tick.
*
* Since the emitted values are shared across all subscribers, it is important
* not to mutate the returned values. The values may be frozen as a precaution.
*
* @param key - Unique key associated with the data
*/
observe$<T>(key: string): Observable<StorageValueChange<T>>;
observe$<T extends JsonValue>(
key: string,
): Observable<StorageValueSnapshot<T>>;
/**
* Returns an immediate snapshot value for the given key, if possible.
*
* @remarks
*
* Combine with {@link StorageApi.observe$} to get notified of value changes.
*
* Note that this method is synchronous, and some underlying storages may be
* unable to retrieve a value using this method - the result may or may not
* consistently have a presence of 'unknown'. Use {@link StorageApi.observe$}
* to be sure to receive an actual value eventually.
*/
snapshot<T extends JsonValue>(key: string): StorageValueSnapshot<T>;
}
/**
@@ -47,12 +47,11 @@
min-height: 100%;
}
</style>
<title><%= app.title %></title>
<% if (app.googleAnalyticsTrackingId && typeof app.googleAnalyticsTrackingId
=== 'string') { %>
<title><%= config.getString('app.title') %></title>
<% if (config.has('app.googleAnalyticsTrackingId')) { %>
<script
async
src="https://www.googletagmanager.com/gtag/js?id=<%= app.googleAnalyticsTrackingId %>"
src="https://www.googletagmanager.com/gtag/js?id=<%= config.getString('app.googleAnalyticsTrackingId') %>"
></script>
<script>
window.dataLayer = window.dataLayer || [];
@@ -61,7 +60,10 @@
}
gtag('js', new Date());
gtag('config', '<%= app.googleAnalyticsTrackingId %>');
gtag(
'config',
'<%= config.getString("app.googleAnalyticsTrackingId") %>',
);
</script>
<% } %>
</head>
@@ -47,7 +47,7 @@
min-height: 100%;
}
</style>
<title><%= app.title %></title>
<title><%= config.getString('app.title') %></title>
</head>
<body style="margin: 0">
<noscript>You need to enable JavaScript to run this app.</noscript>
+5 -2
View File
@@ -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<void>;
@@ -81,11 +82,13 @@ export class MockStorageApi implements StorageApi {
// (undocumented)
get<T>(key: string): T | undefined;
// (undocumented)
observe$<T>(key: string): Observable<StorageValueChange<T>>;
observe$<T>(key: string): Observable<StorageValueSnapshot<T>>;
// (undocumented)
remove(key: string): Promise<void>;
// (undocumented)
set<T>(key: string, data: T): Promise<void>;
// (undocumented)
snapshot<T extends JsonValue>(key: string): StorageValueSnapshot<T>;
}
// @public
@@ -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<void>(resolve => {
storage.observe$<String>('correctKey').subscribe({
storage.observe$<typeof mockData>('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,
});
});
});
@@ -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<T>(key: string): T | undefined {
return this.data[this.getKeyName(key)];
return this.snapshot(key).value as T | undefined;
}
snapshot<T extends JsonValue>(key: string): StorageValueSnapshot<T> {
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<T>(key: string, data: T): Promise<void> {
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<void> {
delete this.data[this.getKeyName(key)];
this.notifyChanges({ key, newValue: undefined });
this.notifyChanges({
key,
presence: 'absent',
value: undefined,
newValue: undefined,
});
}
observe$<T>(key: string): Observable<StorageValueChange<T>> {
observe$<T>(key: string): Observable<StorageValueSnapshot<T>> {
return this.observable.filter(({ key: messageKey }) => messageKey === key);
}
@@ -83,22 +111,22 @@ export class MockStorageApi implements StorageApi {
return `${this.namespace}/${encodeURIComponent(key)}`;
}
private notifyChanges<T>(message: StorageValueChange<T>) {
private notifyChanges<T>(message: StorageValueSnapshot<T>) {
for (const subscription of this.subscribers) {
subscription.next(message);
}
}
private subscribers = new Set<
ZenObservable.SubscriptionObserver<StorageValueChange>
ZenObservable.SubscriptionObserver<StorageValueSnapshot<JsonValue>>
>();
private readonly observable = new ObservableImpl<StorageValueChange>(
subscriber => {
this.subscribers.add(subscriber);
return () => {
this.subscribers.delete(subscriber);
};
},
);
private readonly observable = new ObservableImpl<
StorageValueSnapshot<JsonValue>
>(subscriber => {
this.subscribers.add(subscriber);
return () => {
this.subscribers.delete(subscriber);
};
});
}
@@ -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<string[]>('entityRefs') ?? []);
oldStarredEntities
.filter(isString)
@@ -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 (
<Tooltip title={greeting.language}>
<span>{`${greeting.greeting}, ${profile.displayName || userId}!`}</span>
<span>{`${greeting.greeting}${
profile?.displayName ? `, ${profile?.displayName}` : ''
}!`}</span>
</Tooltip>
);
};
+23
View File
@@ -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<KubernetesAuthProvidersApi>;
// 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<ObjectsByEntityResponse>;
}
// 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)
+1
View File
@@ -16,3 +16,4 @@
export { kubernetesApiRef } from './types';
export type { KubernetesApi } from './types';
export { KubernetesBackendClient } from './KubernetesBackendClient';
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint')],
};
+79
View File
@@ -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 = (
<EntityLayout>
{/* other tabs... */}
<EntityLayout.Route
if={isNewRelicDashboardAvailable}
path="/newrelic-dashboard"
title="New Relic Dashboard"
>
<EntityNewRelicDashboardContent />
</EntityLayout.Route>
```
3. Add the following in `EntityPage.tsx` to display dashboard links in overview page
```
const overviewContent = (
{/* other tabs... */}
<EntitySwitch>
<EntitySwitch.Case if={isNewRelicDashboardAvailable}>
<Grid item md={6} xs={12}>
<EntityNewRelicDashboardCard />
</Grid>
</EntitySwitch.Case>
</EntitySwitch>
```
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: <dashboard_guid>
spec:
type: service
```
All set , you will be able to see the plugin in action!
+38
View File
@@ -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
/// <reference types="react" />
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<undefined>;
},
{}
>;
// (No @packageDocumentation comment for this package)
```
+19
View File
@@ -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();
+46
View File
@@ -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"
}
}
+46
View File
@@ -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 <NewRelicDashboard />;
}
return (
<>
<MissingAnnotationEmptyState annotation={NEWRELIC_GUID_ANNOTATION} />
<Button
variant="contained"
color="primary"
href="https://github.com/backstage/backstage/tree/master/plugins/newrelic-dashboard"
>
Read New Relic Dashboard Plugin Docs
</Button>
</>
);
};
@@ -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<NewRelicDashboardApi>({
id: 'plugin.newrelicdashboard.service',
});
export type NewRelicDashboardApi = {
getDashboardEntity(guid: string): Promise<DashboardEntitySummary | undefined>;
getDashboardSnapshot(
guid: string,
duration: number,
): Promise<DashboardSnapshotSummary | undefined>;
};
@@ -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<T>(
query: string,
variables: { [key in string]: string | number },
): Promise<T | undefined> {
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<DashboardEntitySummary | undefined> {
const DashboardEntityList = await this.callApi<DashboardEntity>(
getDashboardParentGuidQuery,
{
query: `parentId ='${guid}'`,
},
);
return {
getDashboardEntity: DashboardEntityList!,
};
}
async getDashboardSnapshot(
guid: string,
duration: number,
): Promise<DashboardSnapshotSummary | undefined> {
const DashboardSnapshotValue = await this.callApi<DashboardSnapshot>(
getDashboardSnapshotQuery,
{
guid: guid,
duration: duration,
},
);
return {
getDashboardSnapshot: DashboardSnapshotValue!,
};
}
}
@@ -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';
@@ -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<DashboardEntitySummary | undefined> =
newRelicDashboardAPI.getDashboardEntity(
String(entity.metadata.annotations?.[NEWRELIC_GUID_ANNOTATION]),
);
return dashboardObject;
}, [entity.metadata.annotations?.[NEWRELIC_GUID_ANNOTATION]]);
if (loading) {
return <Progress />;
}
if (error) {
return <ErrorPanel title={error.name} defaultExpanded error={error} />;
}
return (
<InfoCard title="New Relic Dashboard Pages" variant="gridItem">
{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 (
<Box style={{ margin: '10px' }} display="flex">
<Box mr={1} className={classes.svgIcon}>
<Typography component="div">
<DesktopMac />
</Typography>
</Box>
<Box flexGrow="1">
<Link to={entityResult.permalink}>{entityResult.name}</Link>
</Box>
</Box>
);
},
)}
</InfoCard>
);
};
@@ -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<DashboardSnapshotSummary | undefined> =
newRelicDashboardAPI.getDashboardSnapshot(guid, duration);
return dashboardObject;
}, [guid, duration]);
if (loading) {
return <Progress />;
}
if (error) {
return <ErrorPanel title={error.name} defaultExpanded error={error} />;
}
const url =
value?.getDashboardSnapshot?.data?.dashboardCreateSnapshotUrl?.replace(
/\pdf$/i,
'png',
);
return (
<Grid container style={{ marginTop: '30px' }}>
<InfoCard variant="gridItem" title={name}>
<Box display="flex">
<Box flexGrow="1">
<Link to={permalink}>
{url ? (
<img
alt={`${name} Dashbord`}
style={{ border: 'solid 1px black' }}
src={url}
/>
) : (
'Dashboard loading... , click here to open if it didnt render correctly'
)}
</Link>
</Box>
</Box>
</InfoCard>
</Grid>
);
};
@@ -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 (
<div
role="tabpanel"
hidden={value1 !== index}
id={`simple-tabpanel-${index}`}
aria-labelledby={`simple-tab-${index}`}
{...other}
>
{children}
</div>
);
}
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<DashboardEntitySummary | undefined> =
newRelicDashboardAPI.getDashboardEntity(guid);
return dashboardObject;
}, [guid]);
const [value1, setValue1] = useState<number>(0);
const handleChange = ({}: React.ChangeEvent<{}>, newValue: number) => {
setValue1(newValue);
};
if (loading) {
return <Progress />;
}
if (error) {
return <ErrorPanel title={error.name} defaultExpanded error={error} />;
}
return (
<Grid container direction="column">
<Tabs
selectionFollowsFocus
indicatorColor="primary"
textColor="inherit"
variant="scrollable"
scrollButtons="auto"
aria-label="scrollable auto tabs example"
onChange={handleChange}
value={value1}
style={{ width: '100%' }}
>
{value?.getDashboardEntity?.data?.actor.entitySearch.results.entities?.map(
(Entity: ResultEntity, index: number) => {
return (
<Tab
label={Entity.name}
className={styles.defaultTab}
classes={{
selected: styles.selected,
root: styles.tabRoot,
}}
{...a11yProps(index)}
/>
);
},
)}
</Tabs>
{value?.getDashboardEntity?.data?.actor.entitySearch.results.entities?.map(
(Entity: ResultEntity, index: number) => {
return (
<TabPanel value1={value1} index={index}>
<DashboardSnapshot
name={Entity.name}
permalink={Entity.permalink}
guid={Entity.guid}
duration={26297430000}
/>
</TabPanel>
);
},
)}
</Grid>
);
};
@@ -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';
@@ -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 (
<Page themeId="home">
<Content>
<Grid container spacing={6} direction="row" alignItems="stretch">
<Grid item xs={12}>
<DashboardEntityList />
</Grid>
<Grid item xs={12}>
<DashboardSnapshotList
guid={String(
entity.metadata.annotations?.[NEWRELIC_GUID_ANNOTATION],
)}
/>
</Grid>
</Grid>
</Content>
</Page>
);
};
@@ -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';
@@ -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';
+21
View File
@@ -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';
@@ -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();
});
});
+62
View File
@@ -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,
),
},
}),
);
@@ -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';
@@ -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}';
+20
View File
@@ -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',
});
@@ -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';
@@ -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<ResultEntity>;
};
};
};
};
};
export type ResultEntity = {
guid: string;
permalink: string;
name: string;
};
@@ -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;
};
};
@@ -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;
};
@@ -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 && <MarkdownContent content={description} />;
@@ -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';
@@ -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 = ({
<StepContent key={title}>
<Form
showErrorList={false}
fields={fields}
fields={{ ...fieldOverrides, ...fields }}
widgets={widgets}
noHtml5Validate
formData={formData}
@@ -60,6 +60,7 @@ type RecommendedDeploymentOptions = {
logger: Logger;
discovery: PluginEndpointDiscovery;
config: Config;
cache?: PluginCacheManager;
};
/**
@@ -89,7 +90,7 @@ export async function createRouter(
// Set up a cache client if configured.
let cache: TechDocsCache | undefined;
const defaultTtl = config.getOptionalNumber('techdocs.cache.ttl');
if (isOutOfTheBoxOption(options) && options.cache && defaultTtl) {
if (options.cache && defaultTtl) {
const cacheClient = options.cache.getClient({ defaultTtl });
cache = TechDocsCache.fromConfig(config, { cache: cacheClient, logger });
}