Merge branch 'master' into bugMemberListCard

Signed-off-by: Stefan Petrovic <stefanp0618@gmail.com>
This commit is contained in:
Stefan Petrovic
2023-08-10 14:25:17 +02:00
16 changed files with 596 additions and 104 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/create-app': patch
---
Bump to a newer version of the `concurrently` library
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend': patch
---
Internal refactor for load test
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-newrelic': patch
---
The newrelic plugin now supports pagination when retrieving results from newrelic. It will no longer truncate results. To see all applications, the link header will need to be allowed through the proxy (see the newrelic plugin readme).
+1 -1
View File
@@ -43,7 +43,7 @@ _You can do this by using the [Adopter form](https://info.backstage.spotify.com/
| [MavTek](https://www.mavtek.com/) | [@fgascon](https://github.com/fgascon) | Developer portal focused on standardizing practices, centralizing documentation and streamlining developer practices. |
| [QuintoAndar](https://www.quintoandar.com.br/) | [@quintoandar](https://github.com/quintoandar) | Developer portal, services catalog and centralization of service metrics. |
| [empathy.co](https://empathy.co/) | [@guillermotti](https://github.com/guillermotti) | Developer portal for tech docs, service catalog, plugin discovery and much more. |
| [creditas.com](https://creditas.com/) | [@aureliosaraiva](https://github.com/aureliosaraiva) [@Creditas](https://github.com/creditas) | Centralization of all services, standards, documentation, etc. We started the deployment process. |
| [creditas.com](https://creditas.com/) | [@rebender](https://github.com/rebender) [@Creditas](https://github.com/creditas) | Centralization of all services, standards, documentation, etc. We started the deployment process. |
| [Prisjakt](https://www.prisjakt.nu) / [PriceSpy](https://pricespy.co.uk) | [@kennylindahl](https://github.com/kennylindahl) | Internal developer portal - Documentation, scaffolding, software catalog, TechRadar, Gitlab org data integration |
| [Powerspike](https://powerspike.tv/) | [@trelore](https://github.com/trelore) | Developer portal for documentation of core libraries and repositories. |
| [2U](https://2u.com) | [@danielleEriksen](https://github.com/danielleEriksen), [@sbhatia](https://github.com/sbhatia) | Development team home-base, promoting service discoverability, resource dependencies, and tech radar |
+2
View File
@@ -73,6 +73,8 @@ proxy:
target: https://api.newrelic.com/v2
headers:
X-Api-Key: ${NEW_RELIC_REST_API_KEY}
allowedHeaders:
- link
'/newrelic/api':
target: https://api.newrelic.com
+12 -12
View File
@@ -2,25 +2,25 @@
id: glossary
title: Backstage Glossary
# prettier-ignore
description: List of all the terms, abbreviations, and phrases used in Backstage, together with their explanations.
description: List of terms, abbreviations, and phrases used in Backstage, together with their explanations.
---
The Backstage Glossary lists all the terms, abbreviations, and phrases used in
The Backstage Glossary lists terms, abbreviations, and phrases used in
Backstage, together with their explanations. We encourage you to use the
terminology below for clarity and consistency when discussing Backstage.
### Authentication Glossary
This [page](../auth/glossary.md) directs to the terms and phrases related to
authentication and identity section of Backstage.
See also [Authentication Glossary](../auth/glossary.md), a separate glossary of terms and phrases
specifically related to the authentication and identity section of Backstage.
### Backstage User Profiles
There are three main user profiles for Backstage: the integrator, the
contributor, and the software engineer.
contributor, and the end user (typically a software engineer).
| Term | Explanation |
| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Integrator | The **integrator** hosts the Backstage app and configures which plugins are available to use in the app. |
| Contributor | The **contributor** adds functionality to the app by writing plugins. |
| Software Engineer | The **software engineer** uses the app's functionality and interacts with its plugins. In practice, this profile covers the various roles that help deliver software, from the Software Engineer themselves, to Designers, Data Scientists, Product Owners, Engineering Managers, etc. |
| Term | Explanation |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Integrator | The **integrator** hosts the Backstage app and configures which plugins are available to use in the app. |
| Contributor | The **contributor** adds functionality to the app by writing plugins. |
| End user | The **end user** uses the app's functionality and interacts with its plugins. This profile covers the various roles that help deliver software. The typical end user is a **software engineer**, but users might also consider themselves _designers_, _data scientists_, _product owners_, _engineering managers_, _technical writers_, and so on. |
| Software engineer | The **software engineer** is an **end user** who uses the app's functionality and interacts with its plugins in the course of writing and documenting code. This user is more likely to embed documentation in the code files they produce, and create rough drafts of conceptual pages in collaboration with a **technical writer** or _technical editor_. |
| Technical writer | The **technical writer** is an **end user** who uses the app's functionality and interacts with its plugins in the course of writing and editing documentation. This user is more likely to produce and customize templates and produce conceptual pages to supplement documentation embedded in code files. |
+1 -1
View File
@@ -65,7 +65,7 @@
"@types/node": "^16.11.26",
"@types/webpack": "^5.28.0",
"command-exists": "^1.2.9",
"concurrently": "^7.0.0",
"concurrently": "^8.0.0",
"cross-env": "^7.0.0",
"e2e-test": "workspace:*",
"eslint": "^8.6.0",
@@ -31,7 +31,7 @@
"devDependencies": {
"@backstage/cli": "^{{version '@backstage/cli'}}",
"@spotify/prettier-config": "^12.0.0",
"concurrently": "^6.0.0",
"concurrently": "^8.0.0",
"lerna": "^4.0.0",
"node-gyp": "^9.0.0",
"prettier": "^2.3.2",
@@ -14,7 +14,6 @@
* limitations under the License.
*/
import { createBackendModule } from '@backstage/backend-plugin-api';
import { Entity } from '@backstage/catalog-model';
import { LocationSpec } from '@backstage/plugin-catalog-common';
import {
@@ -26,7 +25,6 @@ import {
EntityProviderConnection,
processingResult,
} from '@backstage/plugin-catalog-node';
import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha';
/**
* Options for a fixed initial load of entities.
@@ -143,12 +141,15 @@ export const common = {
/**
* The entity provider that drives the initial base entity injection
* @internal
*/
class SyntheticLoadEntitiesProvider implements EntityProvider {
export class SyntheticLoadEntitiesProvider implements EntityProvider {
constructor(
private readonly load: SyntheticLoadOptions,
private readonly events: SyntheticLoadEvents,
) {}
) {
validateSyntheticLoadOptions(load);
}
getProviderName(): string {
return 'SyntheticLoadEntitiesProvider';
@@ -180,9 +181,12 @@ class SyntheticLoadEntitiesProvider implements EntityProvider {
/**
* Supporting processor for emitting children and relations
* @internal
*/
class SyntheticLoadEntitiesProcessor implements CatalogProcessor {
constructor(private readonly load: SyntheticLoadOptions) {}
export class SyntheticLoadEntitiesProcessor implements CatalogProcessor {
constructor(private readonly load: SyntheticLoadOptions) {
validateSyntheticLoadOptions(load);
}
getProcessorName(): string {
return 'SyntheticLoadEntitiesProcessor';
@@ -231,27 +235,3 @@ class SyntheticLoadEntitiesProcessor implements CatalogProcessor {
return entity;
}
}
export const catalogModuleSyntheticLoadEntities = createBackendModule(
(options: { load: SyntheticLoadOptions; events?: SyntheticLoadEvents }) => ({
moduleId: 'syntheticLoadEntities',
pluginId: 'catalog',
register(reg) {
reg.registerInit({
deps: {
catalog: catalogProcessingExtensionPoint,
},
async init({ catalog }) {
const { load, events = {} } = options;
validateSyntheticLoadOptions(load);
const provider = new SyntheticLoadEntitiesProvider(load, events);
const processor = new SyntheticLoadEntitiesProcessor(load);
catalog.addEntityProvider(provider);
catalog.addProcessor(processor);
},
});
},
}),
);
@@ -16,16 +16,19 @@
import {
coreServices,
createBackendModule,
createServiceFactory,
} from '@backstage/backend-plugin-api';
import { TestDatabases, startTestBackend } from '@backstage/backend-test-utils';
import { catalogPlugin } from '@backstage/plugin-catalog-backend/alpha';
import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha';
import { Knex } from 'knex';
import { applyDatabaseMigrations } from '../../database/migrations';
import {
SyntheticLoadEvents,
SyntheticLoadOptions,
catalogModuleSyntheticLoadEntities,
SyntheticLoadEntitiesProvider,
SyntheticLoadEntitiesProcessor,
} from './lib/catalogModuleSyntheticLoadEntities';
import { describePerformanceTest, performanceTraceEnabled } from './lib/env';
@@ -180,10 +183,25 @@ describePerformanceTest('stitchingPerformance', () => {
services: [staticDatabase(knex)],
features: [
catalogPlugin(),
catalogModuleSyntheticLoadEntities({
load,
events: tracker.events(),
}),
createBackendModule({
moduleId: 'syntheticLoadEntities',
pluginId: 'catalog',
register(reg) {
reg.registerInit({
deps: {
catalog: catalogProcessingExtensionPoint,
},
async init({ catalog }) {
catalog.addEntityProvider(
new SyntheticLoadEntitiesProvider(load, tracker.events()),
);
catalog.addProcessor(
new SyntheticLoadEntitiesProcessor(load),
);
},
});
},
})(),
],
});
+4
View File
@@ -18,6 +18,8 @@ APIs.
target: https://api.newrelic.com/v2
headers:
X-Api-Key: ${NEW_RELIC_REST_API_KEY}
allowedHeaders:
- link
```
There is some types of api key on new relic, to this use must be `User` type of key, In your production deployment of Backstage, you would also need to ensure that
@@ -33,6 +35,8 @@ APIs.
'/newrelic/apm/api':
headers:
X-Api-Key: NRRA-YourActualApiKey
allowedHeaders:
- link
```
Read more about how to find or generate this key in
+4 -1
View File
@@ -39,6 +39,7 @@
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.61",
"parse-link-header": "^2.0.0",
"react-use": "^17.2.4"
},
"peerDependencies": {
@@ -47,6 +48,7 @@
"react-router-dom": "6.0.0-beta.0 || ^6.3.0"
},
"devDependencies": {
"@backstage/backend-test-utils": "workspace:^",
"@backstage/cli": "workspace:^",
"@backstage/core-app-api": "workspace:^",
"@backstage/dev-utils": "workspace:^",
@@ -56,9 +58,10 @@
"@testing-library/react": "^12.1.3",
"@testing-library/user-event": "^14.0.0",
"@types/node": "^16.11.26",
"@types/parse-link-header": "^2.0.1",
"@types/react": "^16.13.1 || ^17.0.0",
"cross-fetch": "^3.1.5",
"msw": "^1.0.0"
"msw": "^1.2.3"
},
"files": [
"dist"
+384
View File
@@ -0,0 +1,384 @@
/*
* Copyright 2023 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 { NewRelicApplication, NewRelicClient } from '.';
import { DiscoveryApi } from '@backstage/core-plugin-api';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { MockFetchApi, setupRequestMockHandlers } from '@backstage/test-utils';
const mockedDiscoveryApi: DiscoveryApi = {
getBaseUrl: async () => 'https://test.test',
};
beforeEach(() => {
jest.resetAllMocks();
});
describe('NewRelicClient', () => {
const server = setupServer();
setupRequestMockHandlers(server);
beforeEach(() => {
server.resetHandlers();
});
test.each([
['https://test.test/BASEPATH/apm/api/applications.json', '/BASEPATH'],
['https://test.test/BASEPATH2/apm/api/applications.json', '/BASEPATH2'],
['https://test.testBASEPATH3/apm/api/applications.json', 'BASEPATH3'],
['https://test.test/newrelic/apm/api/applications.json', undefined],
])(
'It correctly forms the request url (%p) when proxyPathBase is %p',
async (expectedUrl, basePathOverride) => {
server.use(
rest.get(expectedUrl, (_, res, ctx) =>
res(ctx.status(200), ctx.json({ applications: [] })),
),
);
const mockedFetchApi = new MockFetchApi();
const fetchSpy = jest.spyOn(mockedFetchApi, 'fetch');
const client = new NewRelicClient({
discoveryApi: mockedDiscoveryApi,
fetchApi: mockedFetchApi,
proxyPathBase: basePathOverride,
});
await client.getApplications();
expect(fetchSpy).toHaveBeenCalledWith(expectedUrl);
},
);
it('Correctly reads all pages of results and returns the expected results', async () => {
const mockedApplicationOne: NewRelicApplication = {
id: 1,
application_summary: {
apdex_score: 0,
error_rate: 100,
host_count: 500,
instance_count: 5000,
response_time: 20,
throughput: 500000,
},
name: 'Testing Application #1',
language: 'en-us',
health_status: 'Failing',
reporting: true,
settings: {
app_apdex_threshold: 0,
end_user_apdex_threshold: 0,
enable_real_user_monitoring: true,
use_server_side_config: true,
},
};
const mockedApplicationTwo: NewRelicApplication = {
id: 2,
application_summary: {
apdex_score: -900,
error_rate: 0,
host_count: 0,
instance_count: 0,
response_time: 0,
throughput: 0,
},
name: 'Testing Application #2',
language: 'en-us',
health_status: 'Working',
reporting: true,
settings: {
app_apdex_threshold: 0,
end_user_apdex_threshold: 0,
enable_real_user_monitoring: true,
use_server_side_config: true,
},
};
const mockedApplicationThree: NewRelicApplication = {
id: 3,
application_summary: {
apdex_score: -900,
error_rate: 0,
host_count: 0,
instance_count: 0,
response_time: 0,
throughput: 0,
},
name: 'Testing Application #3',
language: 'en-us',
health_status: 'Waiting',
reporting: false,
settings: {
app_apdex_threshold: 1000,
end_user_apdex_threshold: 500,
enable_real_user_monitoring: false,
use_server_side_config: false,
},
};
const queryToRequestData = new Map<
string | null,
{ link?: string | string[]; apps: NewRelicApplication[] }
>([
[
null,
{
link: [
'<https://next.page/page2?page=2>; rel="next"',
'<https://badsite.dontgohere>; rel="bad"',
],
apps: [mockedApplicationOne],
},
],
[
'2',
{
link: '<https://next.page/page3?page=3>; rel="next",',
apps: [],
},
],
[
'3',
{
apps: [mockedApplicationTwo, mockedApplicationThree],
},
],
]);
const mockedFetchApi = new MockFetchApi();
const fetchSpy = jest.spyOn(mockedFetchApi, 'fetch');
server.use(
rest.get(
'https://test.test/newrelic/apm/api/applications.json',
(req, res, ctx) => {
const nextPageNumber = req.url.searchParams.get('page');
const requestData = queryToRequestData.get(nextPageNumber) ?? {
apps: [],
};
const { link, apps: applications } = requestData;
const statusTransform = ctx.status(200);
const responseBody = ctx.json({ applications });
if (!!link) {
return res(statusTransform, ctx.set({ link }), responseBody);
}
return res(statusTransform, responseBody);
},
),
);
const client = new NewRelicClient({
discoveryApi: mockedDiscoveryApi,
fetchApi: mockedFetchApi,
});
const actual = await client.getApplications();
const expected = {
applications: [
mockedApplicationOne,
mockedApplicationTwo,
mockedApplicationThree,
],
};
expect(fetchSpy).toHaveBeenCalledTimes(3);
expect(fetchSpy).toHaveBeenCalledWith(
'https://test.test/newrelic/apm/api/applications.json',
);
expect(fetchSpy).toHaveBeenCalledWith(
'https://test.test/newrelic/apm/api/applications.json?page=2',
);
expect(fetchSpy).toHaveBeenCalledWith(
'https://test.test/newrelic/apm/api/applications.json?page=3',
);
expect(actual).toStrictEqual(expected);
});
test.each([['Link'], ['LINK'], ['lINK']])(
'It does not attempt pagination when the link header name is invalid (%p)',
async linkHeaderName => {
const mockedFetchApi = new MockFetchApi();
const fetchSpy = jest.spyOn(mockedFetchApi, 'fetch');
server.use(
rest.get(
'https://test.test/newrelic/apm/api/applications.json',
(_, res, ctx) =>
res(
ctx.status(200),
ctx.set(
linkHeaderName,
'<https://test.test/badroute>; rel="next"',
),
ctx.json({ applications: [] }),
),
),
rest.get('https://test.test/badroute', () => {
throw new Error(
'NewRelicClient attempted to paginate when it should not have',
);
}),
);
const client = new NewRelicClient({
discoveryApi: mockedDiscoveryApi,
fetchApi: mockedFetchApi,
});
await client.getApplications();
expect(fetchSpy).toHaveBeenCalledTimes(1);
expect(fetchSpy).toHaveBeenCalledWith(
'https://test.test/newrelic/apm/api/applications.json',
);
},
);
test.each([
[''],
['<> rel=""'],
['<>; rel=""'],
['<https://next.page>; rel=""'],
['<>; rel:"value"'],
['<https://next.page>; rel: "next"'],
['ABCDE'],
['<https://next.page/page3?page=>; rel="next",'],
])(
'It does not attempt pagination when the link header value is invalid (%p)',
async linkHeaderValue => {
const mockedFetchApi = new MockFetchApi();
const fetchSpy = jest.spyOn(mockedFetchApi, 'fetch');
server.use(
rest.get(
'https://test.test/newrelic/apm/api/applications.json',
(_, res, ctx) =>
res(
ctx.status(200),
ctx.set('link', linkHeaderValue),
ctx.json({ applications: [] }),
),
),
rest.get('https://test.test/badroute', () => {
throw new Error(
'NewRelicClient attempted to paginate when it should not have',
);
}),
);
const client = new NewRelicClient({
discoveryApi: mockedDiscoveryApi,
fetchApi: mockedFetchApi,
});
await client.getApplications();
expect(fetchSpy).toHaveBeenCalledTimes(1);
expect(fetchSpy).toHaveBeenCalledWith(
'https://test.test/newrelic/apm/api/applications.json',
);
},
);
test.each([
['Error communicating with New Relic: Not Found', 404, JSON.stringify({})],
[
'Error communicating with New Relic: ERROR TITLE',
404,
JSON.stringify({
error: {
title: 'ERROR TITLE',
},
}),
],
[
'Error communicating with New Relic: Internal Server Error',
500,
JSON.stringify(undefined),
],
[
'Error communicating with New Relic: Internal Server Error',
500,
JSON.stringify(null),
],
[
'Error communicating with New Relic: Internal Server Error',
500,
'<invalid></invalid',
],
])(
'It throws this error: %p when the status code is %p and the body is %j',
async (expectedErrorMessage, statusCode, body) => {
server.use(
rest.get(
'https://test.test/newrelic/apm/api/applications.json',
(_, res, ctx) => res(ctx.status(statusCode), ctx.body(body)),
),
);
const client = new NewRelicClient({
discoveryApi: mockedDiscoveryApi,
fetchApi: new MockFetchApi(),
});
await expect(client.getApplications()).rejects.toThrow(
expectedErrorMessage,
);
},
);
it('Throws an error when the body is invalid json but the status code is 200', async () => {
server.use(
rest.get(
'https://test.test/newrelic/apm/api/applications.json',
(_, res, ctx) => res(ctx.status(200), ctx.body('<Invalid></Invalid')),
),
);
const client = new NewRelicClient({
discoveryApi: mockedDiscoveryApi,
fetchApi: new MockFetchApi(),
});
await expect(client.getApplications()).rejects.toThrow();
});
it('Generates the base url only once', async () => {
const getBaseUrlSpy = jest.spyOn(mockedDiscoveryApi, 'getBaseUrl');
server.use(
rest.get(
'https://test.test/newrelic/apm/api/applications.json',
(_, res, ctx) => res(ctx.status(200), ctx.json({ applications: [] })),
),
);
const client = new NewRelicClient({
discoveryApi: mockedDiscoveryApi,
fetchApi: new MockFetchApi(),
});
await client.getApplications();
await client.getApplications();
await client.getApplications();
await client.getApplications();
expect(getBaseUrlSpy).toHaveBeenCalledTimes(1);
});
});
+56 -16
View File
@@ -14,7 +14,13 @@
* limitations under the License.
*/
import { createApiRef, DiscoveryApi } from '@backstage/core-plugin-api';
import {
createApiRef,
DiscoveryApi,
FetchApi,
} from '@backstage/core-plugin-api';
import parseLinkHeader from 'parse-link-header';
export type NewRelicApplication = {
id: number;
@@ -61,6 +67,7 @@ const DEFAULT_PROXY_PATH_BASE = '/newrelic';
type Options = {
discoveryApi: DiscoveryApi;
fetchApi: FetchApi;
/**
* Path to use for requests via the proxy, defaults to /newrelic
*/
@@ -71,39 +78,72 @@ export interface NewRelicApi {
getApplications(): Promise<NewRelicApplications>;
}
interface NewRelicPageReadResult {
nextPageUrl: string | undefined;
applicationsFromReadPage: NewRelicApplication[];
}
export class NewRelicClient implements NewRelicApi {
private readonly discoveryApi: DiscoveryApi;
private readonly fetchApi: FetchApi;
private readonly proxyPathBase: string;
private baseUrl: string;
constructor(options: Options) {
this.discoveryApi = options.discoveryApi;
this.fetchApi = options.fetchApi;
this.proxyPathBase = options.proxyPathBase ?? DEFAULT_PROXY_PATH_BASE;
this.baseUrl = '';
}
async getApplications(): Promise<NewRelicApplications> {
const url = await this.getApiUrl('apm', 'applications.json');
const response = await fetch(url);
let responseJson;
try {
responseJson = await response.json();
} catch (e) {
responseJson = { applications: [] };
if (!this.baseUrl) {
const proxyUrl = await this.discoveryApi.getBaseUrl('proxy');
this.baseUrl = `${proxyUrl}${this.proxyPathBase}/apm/api/applications.json`;
}
if (response.status !== 200) {
const applications: NewRelicApplication[] = [];
let targetUrl = this.baseUrl;
do {
const { nextPageUrl, applicationsFromReadPage } =
await this.fetchNewRelic(targetUrl);
targetUrl = nextPageUrl ?? '';
applications.push(...applicationsFromReadPage);
} while (!!targetUrl);
return { applications };
}
private async fetchNewRelic(
targetUrl: string,
): Promise<NewRelicPageReadResult> {
const response = await this.fetchApi.fetch(targetUrl);
if (!response.ok) {
let specificErrorTitle = undefined;
try {
specificErrorTitle = (await response.json())?.error?.title;
} catch (e) {
/* empty */
}
throw new Error(
`Error communicating with New Relic: ${
responseJson?.error?.title || response.statusText
specificErrorTitle || response.statusText
}`,
);
}
return responseJson;
}
const readResponse = (await response.json()) as NewRelicApplications;
const linkHeader = response.headers.get('link');
const parseResult = parseLinkHeader(linkHeader);
const nextPageNumber = parseResult?.next?.page;
private async getApiUrl(product: string, path: string) {
const proxyUrl = await this.discoveryApi.getBaseUrl('proxy');
return `${proxyUrl}${this.proxyPathBase}/${product}/api/${path}`;
return {
nextPageUrl: nextPageNumber && `${this.baseUrl}?page=${nextPageNumber}`,
applicationsFromReadPage: readResponse.applications,
};
}
}
+7 -2
View File
@@ -20,6 +20,7 @@ import {
createPlugin,
createRouteRef,
discoveryApiRef,
fetchApiRef,
createRoutableExtension,
} from '@backstage/core-plugin-api';
@@ -33,8 +34,12 @@ export const newRelicPlugin = createPlugin({
apis: [
createApiFactory({
api: newRelicApiRef,
deps: { discoveryApi: discoveryApiRef },
factory: ({ discoveryApi }) => new NewRelicClient({ discoveryApi }),
deps: {
discoveryApi: discoveryApiRef,
fetchApi: fetchApiRef,
},
factory: ({ discoveryApi, fetchApi }) =>
new NewRelicClient({ discoveryApi, fetchApi }),
}),
],
routes: {
+76 -35
View File
@@ -7530,6 +7530,7 @@ __metadata:
version: 0.0.0-use.local
resolution: "@backstage/plugin-newrelic@workspace:plugins/newrelic"
dependencies:
"@backstage/backend-test-utils": "workspace:^"
"@backstage/cli": "workspace:^"
"@backstage/core-app-api": "workspace:^"
"@backstage/core-components": "workspace:^"
@@ -7545,9 +7546,11 @@ __metadata:
"@testing-library/react": ^12.1.3
"@testing-library/user-event": ^14.0.0
"@types/node": ^16.11.26
"@types/parse-link-header": ^2.0.1
"@types/react": ^16.13.1 || ^17.0.0
cross-fetch: ^3.1.5
msw: ^1.0.0
msw: ^1.2.3
parse-link-header: ^2.0.0
react-use: ^17.2.4
peerDependencies:
react: ^16.13.1 || ^17.0.0
@@ -17602,6 +17605,13 @@ __metadata:
languageName: node
linkType: hard
"@types/parse-link-header@npm:^2.0.1":
version: 2.0.1
resolution: "@types/parse-link-header@npm:2.0.1"
checksum: f76678612511365aefc23704f00f3262fcb1d6bd9c4dd83f783a38e50e3ccf26615f9a62c757952cab5af6f2bd8b3b1763ce391376458afce5fe47c6fe2b80e1
languageName: node
linkType: hard
"@types/passport-auth0@npm:^1.0.5":
version: 1.0.5
resolution: "@types/passport-auth0@npm:1.0.5"
@@ -21309,6 +21319,17 @@ __metadata:
languageName: node
linkType: hard
"cliui@npm:^8.0.1":
version: 8.0.1
resolution: "cliui@npm:8.0.1"
dependencies:
string-width: ^4.2.0
strip-ansi: ^6.0.1
wrap-ansi: ^7.0.0
checksum: 79648b3b0045f2e285b76fb2e24e207c6db44323581e421c3acbd0e86454cba1b37aea976ab50195a49e7384b871e6dfb2247ad7dec53c02454ac6497394cb56
languageName: node
linkType: hard
"clone-buffer@npm:^1.0.0":
version: 1.0.0
resolution: "clone-buffer@npm:1.0.0"
@@ -21865,23 +21886,23 @@ __metadata:
languageName: node
linkType: hard
"concurrently@npm:^7.0.0":
version: 7.6.0
resolution: "concurrently@npm:7.6.0"
"concurrently@npm:^8.0.0":
version: 8.2.0
resolution: "concurrently@npm:8.2.0"
dependencies:
chalk: ^4.1.0
date-fns: ^2.29.1
chalk: ^4.1.2
date-fns: ^2.30.0
lodash: ^4.17.21
rxjs: ^7.0.0
shell-quote: ^1.7.3
spawn-command: ^0.0.2-1
supports-color: ^8.1.0
rxjs: ^7.8.1
shell-quote: ^1.8.1
spawn-command: 0.0.2
supports-color: ^8.1.1
tree-kill: ^1.2.2
yargs: ^17.3.1
yargs: ^17.7.2
bin:
conc: dist/bin/concurrently.js
concurrently: dist/bin/concurrently.js
checksum: f705c9a7960f1b16559ca64958043faeeef6385c0bf30a03d1375e15ab2d96dba4f8166f1bbbb1c85e8da35ca0ce3c353875d71dff2aa132b2357bb533b3332e
checksum: eafe6a4d9b7fda87f55ea285cfc6acd937a5286ceec8991ab48e6cc27c45fce6a5c6f45e18d7555defa15dc7d7e8941bc5a9d1ceaf182e31441d420e00333434
languageName: node
linkType: hard
@@ -22924,10 +22945,12 @@ __metadata:
languageName: node
linkType: hard
"date-fns@npm:^2.16.1, date-fns@npm:^2.18.0, date-fns@npm:^2.29.1":
version: 2.29.3
resolution: "date-fns@npm:2.29.3"
checksum: e01cf5b62af04e05dfff921bb9c9933310ed0e1ae9a81eb8653452e64dc841acf7f6e01e1a5ae5644d0337e9a7f936175fd2cb6819dc122fdd9c5e86c56be484
"date-fns@npm:^2.16.1, date-fns@npm:^2.18.0, date-fns@npm:^2.30.0":
version: 2.30.0
resolution: "date-fns@npm:2.30.0"
dependencies:
"@babel/runtime": ^7.21.0
checksum: f7be01523282e9bb06c0cd2693d34f245247a29098527d4420628966a2d9aad154bd0e90a6b1cf66d37adcb769cd108cf8a7bd49d76db0fb119af5cdd13644f4
languageName: node
linkType: hard
@@ -32858,7 +32881,7 @@ __metadata:
languageName: node
linkType: hard
"msw@npm:^1.0.0, msw@npm:^1.0.1, msw@npm:^1.2.1":
"msw@npm:^1.0.0, msw@npm:^1.0.1, msw@npm:^1.2.1, msw@npm:^1.2.3":
version: 1.2.3
resolution: "msw@npm:1.2.3"
dependencies:
@@ -34359,6 +34382,15 @@ __metadata:
languageName: node
linkType: hard
"parse-link-header@npm:^2.0.0":
version: 2.0.0
resolution: "parse-link-header@npm:2.0.0"
dependencies:
xtend: ~4.0.1
checksum: 0e96c6af9910e8f92084b49b8dc6a10dd58db470847d1499f562576180c1ac5e49d18007697f0d538e5f3efdc8ce1d8777641f3ae225302b74af0dd0578b628e
languageName: node
linkType: hard
"parse-path@npm:^7.0.0":
version: 7.0.0
resolution: "parse-path@npm:7.0.0"
@@ -38117,7 +38149,7 @@ __metadata:
"@types/node": ^16.11.26
"@types/webpack": ^5.28.0
command-exists: ^1.2.9
concurrently: ^7.0.0
concurrently: ^8.0.0
cross-env: ^7.0.0
e2e-test: "workspace:*"
eslint: ^8.6.0
@@ -38201,7 +38233,7 @@ __metadata:
languageName: node
linkType: hard
"rxjs@npm:7.8.0, rxjs@npm:^7.0.0, rxjs@npm:^7.2.0, rxjs@npm:^7.5.1, rxjs@npm:^7.5.5, rxjs@npm:^7.8.0":
"rxjs@npm:7.8.0":
version: 7.8.0
resolution: "rxjs@npm:7.8.0"
dependencies:
@@ -38219,6 +38251,15 @@ __metadata:
languageName: node
linkType: hard
"rxjs@npm:^7.2.0, rxjs@npm:^7.5.1, rxjs@npm:^7.5.5, rxjs@npm:^7.8.0, rxjs@npm:^7.8.1":
version: 7.8.1
resolution: "rxjs@npm:7.8.1"
dependencies:
tslib: ^2.1.0
checksum: de4b53db1063e618ec2eca0f7965d9137cabe98cf6be9272efe6c86b47c17b987383df8574861bcced18ebd590764125a901d5506082be84a8b8e364bf05f119
languageName: node
linkType: hard
"sade@npm:^1.7.3":
version: 1.8.1
resolution: "sade@npm:1.8.1"
@@ -38730,10 +38771,10 @@ __metadata:
languageName: node
linkType: hard
"shell-quote@npm:^1.7.3":
version: 1.7.3
resolution: "shell-quote@npm:1.7.3"
checksum: aca58e73a3a5d933d02e0bdddedc53ee14f7c2ec264f97ac915b9d4482d077a38e422aa664631d60a672cd3cdb4054eb2e6c0303f54882453dacb6483e482d34
"shell-quote@npm:^1.7.3, shell-quote@npm:^1.8.1":
version: 1.8.1
resolution: "shell-quote@npm:1.8.1"
checksum: 5f01201f4ef504d4c6a9d0d283fa17075f6770bfbe4c5850b074974c68062f37929ca61700d95ad2ac8822e14e8c4b990ca0e6e9272e64befd74ce5e19f0736b
languageName: node
linkType: hard
@@ -39119,10 +39160,10 @@ __metadata:
languageName: node
linkType: hard
"spawn-command@npm:^0.0.2-1":
version: 0.0.2-1
resolution: "spawn-command@npm:0.0.2-1"
checksum: 2cac8519332193d1ed37d57298c4a1f73095e9edd20440fbab4aa47f531da83831734f2b51c44bb42b2747bf3485dec3fa2b0a1003f74c67561f2636622e328b
"spawn-command@npm:0.0.2, spawn-command@npm:^0.0.2-1":
version: 0.0.2
resolution: "spawn-command@npm:0.0.2"
checksum: e35c5d28177b4d461d33c88cc11f6f3a5079e2b132c11e1746453bbb7a0c0b8a634f07541a2a234fa4758239d88203b758def509161b651e81958894c0b4b64b
languageName: node
linkType: hard
@@ -42736,7 +42777,7 @@ __metadata:
languageName: node
linkType: hard
"xtend@npm:^4.0.0":
"xtend@npm:^4.0.0, xtend@npm:~4.0.1":
version: 4.0.2
resolution: "xtend@npm:4.0.2"
checksum: ac5dfa738b21f6e7f0dd6e65e1b3155036d68104e67e5d5d1bde74892e327d7e5636a076f625599dc394330a731861e87343ff184b0047fef1360a7ec0a5a36a
@@ -42823,7 +42864,7 @@ __metadata:
languageName: node
linkType: hard
"yargs-parser@npm:^21.0.0":
"yargs-parser@npm:^21.1.1":
version: 21.1.1
resolution: "yargs-parser@npm:21.1.1"
checksum: ed2d96a616a9e3e1cc7d204c62ecc61f7aaab633dcbfab2c6df50f7f87b393993fe6640d017759fe112d0cb1e0119f2b4150a87305cc873fd90831c6a58ccf1c
@@ -42874,18 +42915,18 @@ __metadata:
languageName: node
linkType: hard
"yargs@npm:^17.0.0, yargs@npm:^17.1.1, yargs@npm:^17.2.1, yargs@npm:^17.3.1":
version: 17.5.1
resolution: "yargs@npm:17.5.1"
"yargs@npm:^17.0.0, yargs@npm:^17.1.1, yargs@npm:^17.2.1, yargs@npm:^17.3.1, yargs@npm:^17.7.2":
version: 17.7.2
resolution: "yargs@npm:17.7.2"
dependencies:
cliui: ^7.0.2
cliui: ^8.0.1
escalade: ^3.1.1
get-caller-file: ^2.0.5
require-directory: ^2.1.1
string-width: ^4.2.3
y18n: ^5.0.5
yargs-parser: ^21.0.0
checksum: 00d58a2c052937fa044834313f07910fd0a115dec5ee35919e857eeee3736b21a4eafa8264535800ba8bac312991ce785ecb8a51f4d2cc8c4676d865af1cfbde
yargs-parser: ^21.1.1
checksum: 73b572e863aa4a8cbef323dd911d79d193b772defd5a51aab0aca2d446655216f5002c42c5306033968193bdbf892a7a4c110b0d77954a7fdf563e653967b56a
languageName: node
linkType: hard