Merge branch 'backstage:master' into core-components-docs-update
This commit is contained in:
@@ -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.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-kubernetes': minor
|
||||
---
|
||||
|
||||
Includes `KubernetesBackendClient` in the export to allow developers to use it externally.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/core-app-api': minor
|
||||
---
|
||||
|
||||
Updated `WebStorageApi` to reflect the `StorageApi` changes in `@backstage/core-plugin-api`.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/test-utils': minor
|
||||
---
|
||||
|
||||
Updated `MockStorageApi` to reflect the `StorageApi` changes in `@backstage/core-plugin-api`.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-newrelic-dashboard': patch
|
||||
---
|
||||
|
||||
Created a New Plugin , which imports dashboard links and snapshots from New Relic
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-home': patch
|
||||
---
|
||||
|
||||
Fix undefined identity bug in `WelcomeTitle` caused by using deprecated methods of the IdentityApi
|
||||
@@ -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") %>',
|
||||
+ );
|
||||
```
|
||||
@@ -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.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/core-components': patch
|
||||
---
|
||||
|
||||
Do not `setState` when unmounted in `OverflowTooltip`
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'embedded-techdocs-app': patch
|
||||
---
|
||||
|
||||
The `index.html` template was updated to use the new `config` global.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-techdocs-backend': patch
|
||||
---
|
||||
|
||||
Fixed a bug preventing cache from being enabled in TechDocs "recommended" deployment mode.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder': patch
|
||||
---
|
||||
|
||||
Add DescriptionField override to support Markdown
|
||||
@@ -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
|
||||
---
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
id: adrs-overview
|
||||
title: Architecture Decision Records (ADR)
|
||||
sidebar_label: Overview
|
||||
# prettier-ignore
|
||||
description: Overview of Architecture Decision Records (ADR)
|
||||
---
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -16,3 +16,4 @@
|
||||
|
||||
export { kubernetesApiRef } from './types';
|
||||
export type { KubernetesApi } from './types';
|
||||
export { KubernetesBackendClient } from './KubernetesBackendClient';
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
extends: [require.resolve('@backstage/cli/config/eslint')],
|
||||
};
|
||||
@@ -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!
|
||||
@@ -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)
|
||||
```
|
||||
@@ -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();
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
+82
@@ -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>
|
||||
);
|
||||
};
|
||||
+148
@@ -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>
|
||||
);
|
||||
};
|
||||
+17
@@ -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';
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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}';
|
||||
@@ -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;
|
||||
};
|
||||
+22
@@ -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 });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user