Merge remote-tracking branch 'refs/remotes/origin/master' into gauge-with-decimal-digits

This commit is contained in:
Fabian Mack
2024-06-06 14:27:58 +02:00
31 changed files with 318 additions and 284 deletions
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/plugin-scaffolder': patch
'@backstage/plugin-techdocs': patch
---
Internal updates to allow reusing Backstage's `fetchApi` implementation for event source requests. This allows you to for example, override the `Authorization` header.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-auth-backend-module-oidc-provider': patch
---
if oidc server do not provide revocation_endpointwe should not call revoke function
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/core-components': patch
---
Removed max width from `Select` component.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/create-app': patch
---
Updated `node-gyp` to v10
+1
View File
@@ -270,3 +270,4 @@ _You can do this by using the [Adopter form](https://info.backstage.spotify.com/
| [OP Financial Group](https://www.op.fi/op-financial-group) | [Heikki Hellgren](https://github.com/drodil), [Jyrki Koistinen](https://github.com/snyvision) | We are using Backstage as a gateway into our internal development platform offering to simplify complexity. |
| [Scania](https://www.scania.com) | [Santosh Rangavajjula](https://linkedin.com/in/rsbth) | We are implementing backstage at Scania to consolidate operational information from Gitlab, Jira, Confluence, Artifactory, Servicenow and other tools into one place. |
| [Senora.dev](https://senora.dev) | [Shaked Braimok Yosef](https://github.com/ShakedBraimok) | We are using Backstage as a service catalog for our costumers. |
| [Covestro](https://www.covestro.com) | [Julien Gedeon](https://github.com/cvvcv1) | We use Backstage as our Internal developer portal for the Digital R&D department. |
+48 -2
View File
@@ -27,6 +27,45 @@ any configuration. They generate self-signed tokens automatically for making
requests to other Backstage backend plugins, and the receivers use the caller's
public key set endpoint to be able to perform verification.
A backend plugin wishing to make a request to another backend plugin acquires
the required token as follows, where `auth` and `httpAuth` are assumed to be
injected from `coreServices.auth` and `coreServices.httpAuth`, respectively:
```ts
const credentials = await httpAuth.credentials(req);
const { token } = await auth.getPluginRequestToken({
onBehalfOf: credentials,
targetPluginId: '<plugin-id>', // e.g. 'catalog'
});
```
In this example we are assuming that we are in an Express request handler, and
we extract the caller credentials (typically a user or a service) out of its
`req` and make the upstream request on-behalf-of that principal. Prefer to use
this pattern wherever there's an incoming set of credentials to refer to.
If you want to initiate a request entirely as your own service, not on behalf of
anybody else, you can do so as follows:
```ts
const { token } = await auth.getPluginRequestToken({
onBehalfOf: await auth.getOwnServiceCredentials(),
targetPluginId: '<plugin-id>', // e.g. 'catalog'
});
```
Callers pass along the tokens verbatim with requests in the `Authorization`
header:
```yaml
Authorization: Bearer eyJhbG...
```
You may occasionally also see some code, e.g. clients to other systems, that
accept a `credentials` argument directly instead of a token. For those, just
pass in the credentials as acquired above, instead of making a token. The client
code will know what to do with those credentials internally.
This flow has only one configuration option to set in your app-config:
`backend.auth.dangerouslyDisableDefaultAuthPolicy`, which can be set to `true`
if you for some reason need to completely disable both the issuing and
@@ -73,8 +112,8 @@ The subjects must be strings without whitespace. They are used for identifying
each caller, and become part of the credentials object that request recipient
plugins get.
Callers pass along the tokens verbatim with requests in the `Authorization`
header:
Callers must pass along tokens verbatim with requests in the `Authorization`
header when calling Backstage plugins:
```yaml
Authorization: Bearer eZv5o+fW3KnR3kVabMW4ZcDNLPl8nmMW
@@ -125,6 +164,13 @@ credentials object that the request recipient plugins get. All subjects will hav
`external:`, but you can also provide a custom subjectPrefix which will get appended before the
subject returned from your JWKS service (ex. `external:custom-prefix:sub`).
Callers must pass along tokens with requests in the `Authorization` header when
calling Backstage plugins:
```yaml
Authorization: Bearer eyJhbG...
```
## Legacy Tokens
Plugins and backends that are _not_ on the new backend system use a legacy token
+5 -1
View File
@@ -19,7 +19,11 @@ The diagram below provides an overview of the different building blocks, and the
![backend system building blocks diagram](../../assets/backend-system/architecture-building-blocks.drawio.svg)
> NOTE: These are all concepts that existed in our old backend system in one way or another, but they have now all been lifted up to be first class concerns.
:::note Note
These are all concepts that existed in our old backend system in one way or another, but they have now all been lifted up to be first class concerns.
:::
### Backend
@@ -192,7 +192,11 @@ When defining a default factory for a service, it is possible for it to end up w
## Service Factory Options
> NOTE: This pattern is discouraged, only use it when necessary. If possible you should prefer to make services configurable via static configuration instead.
:::note Note
This pattern is discouraged, only use it when necessary. If possible you should prefer to make services configurable via static configuration instead.
:::
When declaring a service factory it's possible to include an options callback. This allows you to customize the factory through code when installing it in the backend. For example, this is how you install an explicit factory instance in the backend without any options:
@@ -6,8 +6,12 @@ sidebar_label: Overview
description: Building backend plugins and modules using the new backend system
---
> NOTE: If you have an existing backend and/or backend plugins that are not yet
> using the new backend system, see [migrating](./08-migrating.md).
:::note Note
If you have an existing backend and/or backend plugins that are not yet
using the new backend system, see [migrating](./08-migrating.md).
:::
This section covers how to build your own backend [plugins](../architecture/04-plugins.md) and
[modules](../architecture/06-modules.md). They are sometimes collectively referred to as
@@ -10,7 +10,11 @@ description: Testing plugins in the frontend system
# Testing Frontend Plugins
> NOTE: The new frontend system is in alpha, and some plugins do not yet fully implement it.
:::note Note
The new frontend system is in alpha, and some plugins do not yet fully implement it.
:::
Utilities for testing frontend features and components are available in `@backstage/frontend-test-utils`.
+14
View File
@@ -0,0 +1,14 @@
---
title: InfraWallet
author: Electrolux Group
authorUrl: https://opensource.electrolux.one/
category: FinOps
description: Control your cloud costs just in the way how you control your bank accounts.
documentation: https://github.com/electrolux-oss/infrawallet/blob/main/README.md
iconUrl: /img/infrawallet-logo.png
npmPackageName: '@electrolux-oss/plugin-infrawallet'
tags:
- finops
- cost
- infrastructure
addedDate: '2024-06-03'
Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

@@ -78,7 +78,6 @@ const useStyles = makeStyles(
createStyles({
formControl: {
margin: theme.spacing(1, 0),
maxWidth: 300,
},
label: {
transform: 'initial',
@@ -37,7 +37,7 @@
"@spotify/prettier-config": "^12.0.0",
"concurrently": "^8.0.0",
"lerna": "^7.3.0",
"node-gyp": "^9.0.0",
"node-gyp": "^10.0.0",
"prettier": "^2.3.2",
"typescript": "~5.4.0"
},
@@ -41,7 +41,7 @@
"app": "link:../app",
"better-sqlite3": "^9.0.0",
"dockerode": "^3.3.1",
"node-gyp": "^9.0.0",
"node-gyp": "^10.0.0",
"pg": "^8.11.3",
"winston": "^3.2.1"
},
@@ -493,5 +493,52 @@ describe('oidcAuthenticator', () => {
new Error('Refresh failed'),
);
});
it('should not revoke refreshToken when issuer revocation_endpoint is undefined', async () => {
const refreshToken = 'revokeRefreshToken2';
const refreshRequest = {
scope: 'testScope',
refreshToken,
req: {} as express.Request,
};
const logoutRequest = {
refreshToken,
req: {} as express.Request,
};
// override .well-known endpoint response, set revocation_endpoint to undefined
mswServer.use(
rest.get(
'https://oidc.test/.well-known/openid-configuration',
(_req, res, ctx) =>
res(
ctx.status(200),
ctx.set('Content-Type', 'application/json'),
ctx.json({
...issuerMetadata,
revocation_endpoint: undefined,
}),
),
),
);
const newImplementation = oidcAuthenticator.initialize({
callbackUrl: 'https://backstage.test/callback',
config: new ConfigReader({
metadataUrl: 'https://oidc.test/.well-known/openid-configuration',
clientId: 'clientId',
clientSecret: 'clientSecret',
}),
});
await oidcAuthenticator.logout?.(logoutRequest, newImplementation);
const refreshResponse = await oidcAuthenticator.refresh(
refreshRequest,
newImplementation,
);
expect(refreshResponse.session.refreshToken).toBe('refreshToken');
});
});
});
@@ -202,8 +202,15 @@ export const oidcAuthenticator = createOAuthAuthenticator({
async logout(input, ctx) {
const { client } = await ctx.promise;
const issuer = client.issuer;
/**
* https://github.com/panva/node-openid-client/blob/main/lib/client.js#L1398
* client.revoke will check revocation_endpoint and if undefined throw error
*
* if oidc server do not provide revocation_endpointwe should not call revoke function
*/
if (input.refreshToken) {
if (input.refreshToken && issuer.revocation_endpoint) {
await client.revoke(input.refreshToken);
}
},
+1 -1
View File
@@ -70,6 +70,7 @@
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.61",
"@microsoft/fetch-event-source": "^2.0.1",
"@react-hookz/web": "^24.0.0",
"@rjsf/core": "5.18.2",
"@rjsf/material-ui": "5.18.2",
@@ -78,7 +79,6 @@
"@types/react": "^16.13.1 || ^17.0.0 || ^18.0.0",
"@uiw/react-codemirror": "^4.9.3",
"classnames": "^2.2.6",
"event-source-polyfill": "^1.0.31",
"git-url-parse": "^14.0.0",
"humanize-duration": "^3.25.1",
"json-schema": "^0.4.0",
+26 -29
View File
@@ -20,14 +20,13 @@ import { MockFetchApi, setupRequestMockHandlers } from '@backstage/test-utils';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { ScaffolderClient } from './api';
import { EventSourcePolyfill } from 'event-source-polyfill';
import { fetchEventSource } from '@microsoft/fetch-event-source';
const MockedEventSource = EventSourcePolyfill as jest.MockedClass<
typeof EventSourcePolyfill
jest.mock('@microsoft/fetch-event-source');
const mockFetchEventSource = fetchEventSource as jest.MockedFunction<
typeof fetchEventSource
>;
jest.mock('event-source-polyfill');
const server = setupServer();
describe('api', () => {
@@ -87,26 +86,23 @@ describe('api', () => {
describe('streamEvents', () => {
describe('eventsource', () => {
it('should work', async () => {
MockedEventSource.prototype.addEventListener.mockImplementation(
(type, fn) => {
if (typeof fn !== 'function') {
return;
}
if (type === 'log') {
fn({
data: '{"id":1,"taskId":"a-random-id","type":"log","createdAt":"","body":{"message":"My log message"}}',
} as any);
} else if (type === 'completion') {
fn({
data: '{"id":2,"taskId":"a-random-id","type":"completion","createdAt":"","body":{"message":"Finished!"}}',
} as any);
}
},
);
const token = 'fake-token';
identityApi.getCredentials.mockResolvedValue({ token: token });
mockFetchEventSource.mockImplementation(async (_url, options) => {
const { onopen, onmessage } = options;
await Promise.resolve();
await onopen?.({ ok: true } as Response);
await Promise.resolve();
onmessage?.({
id: '',
event: 'log',
data: '{"id":1,"taskId":"a-random-id","type":"log","createdAt":"","body":{"message":"My log message"}}',
});
await Promise.resolve();
onmessage?.({
id: '',
event: 'completion',
data: '{"id":2,"taskId":"a-random-id","type":"completion","createdAt":"","body":{"message":"Finished!"}}',
});
});
const next = jest.fn();
@@ -116,14 +112,15 @@ describe('api', () => {
.subscribe({ next, complete });
});
expect(MockedEventSource).toHaveBeenCalledWith(
expect(mockFetchEventSource).toHaveBeenCalledWith(
'http://backstage/api/v2/tasks/a-random-task-id/eventstream',
{
withCredentials: true,
headers: { Authorization: `Bearer ${token}` },
fetch: fetchApi.fetch,
onmessage: expect.any(Function),
onerror: expect.any(Function),
signal: expect.any(AbortSignal),
},
);
expect(MockedEventSource.prototype.close).toHaveBeenCalled();
expect(next).toHaveBeenCalledTimes(2);
expect(next).toHaveBeenCalledWith({
+25 -22
View File
@@ -41,7 +41,10 @@ import {
} from '@backstage/plugin-scaffolder-react';
import queryString from 'qs';
import { EventSourcePolyfill } from 'event-source-polyfill';
import {
EventSourceMessage,
fetchEventSource,
} from '@microsoft/fetch-event-source';
/**
* An API to interact with the scaffolder backend.
@@ -226,11 +229,8 @@ export class ScaffolderClient implements ScaffolderApi {
params.set('after', String(Number(after)));
}
Promise.all([
this.discoveryApi.getBaseUrl('scaffolder'),
this.identityApi?.getCredentials(),
]).then(
([baseUrl, credentials]) => {
this.discoveryApi.getBaseUrl('scaffolder').then(
baseUrl => {
const url = `${baseUrl}/v2/tasks/${encodeURIComponent(
taskId,
)}/eventstream`;
@@ -245,22 +245,25 @@ export class ScaffolderClient implements ScaffolderApi {
}
};
const eventSource = new EventSourcePolyfill(url, {
withCredentials: true,
headers: credentials?.token
? { Authorization: `Bearer ${credentials.token}` }
: {},
});
eventSource.addEventListener('log', processEvent);
eventSource.addEventListener('recovered', processEvent);
eventSource.addEventListener('cancelled', processEvent);
eventSource.addEventListener('completion', (event: any) => {
processEvent(event);
eventSource.close();
subscriber.complete();
});
eventSource.addEventListener('error', event => {
subscriber.error(event);
const ctrl = new AbortController();
fetchEventSource(url, {
fetch: this.fetchApi.fetch,
signal: ctrl.signal,
onmessage(e: EventSourceMessage) {
if (e.event === 'log') {
processEvent(e);
return;
} else if (e.event === 'completion') {
processEvent(e);
subscriber.complete();
ctrl.abort();
return;
}
processEvent(e);
},
onerror(err) {
subscriber.error(err);
},
});
},
error => {
-3
View File
@@ -16,9 +16,6 @@
import '@testing-library/jest-dom';
const { EventSourcePolyfill } = jest.requireMock('event-source-polyfill');
global.EventSource = EventSourcePolyfill;
// Patch jsdom to add feature used by CodeMirror
document.createRange = () => {
const range = new Range();
+1 -3
View File
@@ -465,8 +465,8 @@ export class TechDocsStorageClient implements TechDocsStorageApi_2 {
constructor(options: {
configApi: Config;
discoveryApi: DiscoveryApi;
identityApi: IdentityApi;
fetchApi: FetchApi;
identityApi?: IdentityApi;
});
// (undocumented)
configApi: Config;
@@ -485,8 +485,6 @@ export class TechDocsStorageClient implements TechDocsStorageApi_2 {
getEntityDocs(entityId: CompoundEntityRef, path: string): Promise<string>;
// (undocumented)
getStorageUrl(): Promise<string>;
// (undocumented)
identityApi: IdentityApi;
syncEntityDocs(
entityId: CompoundEntityRef,
logHandler?: (line: string) => void,
+1 -6
View File
@@ -25,11 +25,7 @@ import {
techdocsStorageApiRef,
} from '../src';
import {
configApiRef,
discoveryApiRef,
identityApiRef,
} from '@backstage/core-plugin-api';
import { configApiRef, discoveryApiRef } from '@backstage/core-plugin-api';
import { TechDocsReaderPageProvider } from '@backstage/plugin-techdocs-react';
import { Header, Page, TabbedLayout } from '@backstage/core-components';
@@ -135,7 +131,6 @@ createDevApp()
deps: {
configApi: configApiRef,
discoveryApi: discoveryApiRef,
identityApi: identityApiRef,
},
factory: () => apiBridge,
})
+1 -2
View File
@@ -70,9 +70,9 @@
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.61",
"@material-ui/styles": "^4.10.0",
"@microsoft/fetch-event-source": "^2.0.1",
"@types/react": "^16.13.1 || ^17.0.0 || ^18.0.0",
"dompurify": "^3.0.0",
"event-source-polyfill": "1.0.25",
"git-url-parse": "^14.0.0",
"jss": "~10.10.0",
"lodash": "^4.17.21",
@@ -90,7 +90,6 @@
"@testing-library/react": "^15.0.0",
"@testing-library/user-event": "^14.0.0",
"@types/dompurify": "^3.0.0",
"@types/event-source-polyfill": "^1.0.0",
"canvas": "^2.10.2"
},
"peerDependencies": {
+1 -4
View File
@@ -29,7 +29,6 @@ import {
createApiFactory,
discoveryApiRef,
fetchApiRef,
identityApiRef,
} from '@backstage/core-plugin-api';
import {
compatWrapper,
@@ -55,14 +54,12 @@ const techDocsStorageApi = createApiExtension({
deps: {
configApi: configApiRef,
discoveryApi: discoveryApiRef,
identityApi: identityApiRef,
fetchApi: fetchApiRef,
},
factory: ({ configApi, discoveryApi, identityApi, fetchApi }) =>
factory: ({ configApi, discoveryApi, fetchApi }) =>
new TechDocsStorageClient({
configApi,
discoveryApi,
identityApi,
fetchApi,
}),
}),
+54 -97
View File
@@ -17,16 +17,15 @@
import { UrlPatternDiscovery } from '@backstage/core-app-api';
import { IdentityApi } from '@backstage/core-plugin-api';
import { NotFoundError } from '@backstage/errors';
import { EventSourcePolyfill } from 'event-source-polyfill';
import { fetchEventSource } from '@microsoft/fetch-event-source';
import { MockConfigApi, MockFetchApi } from '@backstage/test-utils';
import { TechDocsStorageClient } from './client';
const MockedEventSource = EventSourcePolyfill as jest.MockedClass<
typeof EventSourcePolyfill
jest.mock('@microsoft/fetch-event-source');
const mockFetchEventSource = fetchEventSource as jest.MockedFunction<
typeof fetchEventSource
>;
jest.mock('event-source-polyfill');
const mockEntity = {
kind: 'Component',
namespace: 'default',
@@ -51,7 +50,6 @@ describe('TechDocsStorageClient', () => {
const storageApi = new TechDocsStorageClient({
configApi,
discoveryApi,
identityApi,
fetchApi,
});
@@ -72,7 +70,6 @@ describe('TechDocsStorageClient', () => {
const storageApi = new TechDocsStorageClient({
configApi,
discoveryApi,
identityApi,
fetchApi,
});
@@ -84,71 +81,50 @@ describe('TechDocsStorageClient', () => {
});
describe('syncEntityDocs', () => {
it('should create eventsource without headers', async () => {
it('should create eventsource with fetch', async () => {
const storageApi = new TechDocsStorageClient({
configApi,
discoveryApi,
identityApi,
fetchApi,
});
MockedEventSource.prototype.addEventListener.mockImplementation(
(type, fn) => {
if (type === 'finish' && typeof fn === 'function') {
fn({ data: '{"updated": false}' } as any);
}
},
);
mockFetchEventSource.mockImplementation(async (_url, options) => {
const { onopen, onmessage } = options;
await Promise.resolve();
await onopen?.({ ok: true } as Response);
await Promise.resolve();
onmessage?.({ id: '', event: 'finish', data: '{"updated": false}' });
});
identityApi.getCredentials.mockResolvedValue({});
await storageApi.syncEntityDocs(mockEntity);
expect(MockedEventSource).toHaveBeenCalledWith(
expect(mockFetchEventSource).toHaveBeenCalledWith(
'http://backstage:9191/api/techdocs/sync/default/Component/test-component',
{ withCredentials: true, headers: {} },
);
});
it('should create eventsource with headers', async () => {
const storageApi = new TechDocsStorageClient({
configApi,
discoveryApi,
identityApi,
fetchApi,
});
MockedEventSource.prototype.addEventListener.mockImplementation(
(type, fn) => {
if (type === 'finish' && typeof fn === 'function') {
fn({ data: '{"updated": false}' } as any);
}
{
fetch: fetchApi.fetch,
onerror: expect.any(Function),
onmessage: expect.any(Function),
signal: expect.any(AbortSignal),
},
);
identityApi.getCredentials.mockResolvedValue({ token: 'token' });
await storageApi.syncEntityDocs(mockEntity);
expect(MockedEventSource).toHaveBeenCalledWith(
'http://backstage:9191/api/techdocs/sync/default/Component/test-component',
{ withCredentials: true, headers: { Authorization: 'Bearer token' } },
);
});
it('should resolve to cached', async () => {
const myFetchApi = new MockFetchApi({
injectIdentityAuth: { identityApi },
});
const storageApi = new TechDocsStorageClient({
configApi,
discoveryApi,
identityApi,
fetchApi,
fetchApi: myFetchApi,
});
mockFetchEventSource.mockImplementation(async (_url, options) => {
const { onopen, onmessage } = options;
await Promise.resolve();
await onopen?.({ ok: true } as Response);
await Promise.resolve();
onmessage?.({ id: '', event: 'finish', data: '{"updated": false}' });
});
MockedEventSource.prototype.addEventListener.mockImplementation(
(type, fn) => {
if (type === 'finish' && typeof fn === 'function') {
fn({ data: '{"updated": false}' } as any);
}
},
);
identityApi.getCredentials.mockResolvedValue({});
await expect(storageApi.syncEntityDocs(mockEntity)).resolves.toEqual(
@@ -160,17 +136,15 @@ describe('TechDocsStorageClient', () => {
const storageApi = new TechDocsStorageClient({
configApi,
discoveryApi,
identityApi,
fetchApi,
});
MockedEventSource.prototype.addEventListener.mockImplementation(
(type, fn) => {
if (type === 'finish' && typeof fn === 'function') {
fn({ data: '{"updated": true}' } as any);
}
},
);
mockFetchEventSource.mockImplementation(async (_url, options) => {
const { onopen, onmessage } = options;
await Promise.resolve();
await onopen?.({ ok: true } as Response);
await Promise.resolve();
onmessage?.({ id: '', event: 'finish', data: '{"updated": true}' });
});
identityApi.getCredentials.mockResolvedValue({});
await expect(storageApi.syncEntityDocs(mockEntity)).resolves.toEqual(
@@ -182,21 +156,18 @@ describe('TechDocsStorageClient', () => {
const storageApi = new TechDocsStorageClient({
configApi,
discoveryApi,
identityApi,
fetchApi,
});
MockedEventSource.prototype.addEventListener.mockImplementation(
(type, fn) => {
if (type === 'log' && typeof fn === 'function') {
fn({ data: '"A log message"' } as any);
}
if (type === 'finish' && typeof fn === 'function') {
fn({ data: '{"updated": false}' } as any);
}
},
);
mockFetchEventSource.mockImplementation(async (_url, options) => {
const { onopen, onmessage } = options;
await Promise.resolve();
await onopen?.({ ok: true } as Response);
await Promise.resolve();
onmessage?.({ id: '', event: 'log', data: '"A log message"' });
await Promise.resolve();
onmessage?.({ id: '', event: 'finish', data: '{"updated": false}' });
});
identityApi.getCredentials.mockResolvedValue({});
const logHandler = jest.fn();
@@ -212,25 +183,18 @@ describe('TechDocsStorageClient', () => {
const storageApi = new TechDocsStorageClient({
configApi,
discoveryApi,
identityApi,
fetchApi,
});
mockFetchEventSource.mockImplementation(async (_url, options) => {
const { onerror } = options;
onerror?.(new NotFoundError('Some not found warning'));
});
// we await later after we emitted the error
identityApi.getCredentials.mockResolvedValue({});
const promise = storageApi.syncEntityDocs(mockEntity).then();
// flush the event loop
await new Promise(r => setTimeout(r));
const instance = MockedEventSource.mock
.instances[0] as jest.Mocked<EventSource>;
instance.onerror?.({
status: 404,
message: 'Some not found warning',
} as any);
await expect(promise).rejects.toThrow(NotFoundError);
await expect(promise).rejects.toThrow('Some not found warning');
});
@@ -239,7 +203,6 @@ describe('TechDocsStorageClient', () => {
const storageApi = new TechDocsStorageClient({
configApi,
discoveryApi,
identityApi,
fetchApi,
});
@@ -247,16 +210,10 @@ describe('TechDocsStorageClient', () => {
identityApi.getCredentials.mockResolvedValue({});
const promise = storageApi.syncEntityDocs(mockEntity).then();
// flush the event loop
await new Promise(r => setTimeout(r));
const instance = MockedEventSource.mock
.instances[0] as jest.Mocked<EventSource>;
instance.onerror?.({
type: 'error',
data: 'Some other error',
} as any);
mockFetchEventSource.mockImplementation(async (_url, options) => {
const { onerror } = options;
onerror?.(new Error('Some other error'));
});
await expect(promise).rejects.toThrow(Error);
await expect(promise).rejects.toThrow('Some other error');
+26 -42
View File
@@ -29,7 +29,7 @@ import {
TechDocsMetadata,
TechDocsStorageApi,
} from '@backstage/plugin-techdocs-react';
import { EventSourcePolyfill } from 'event-source-polyfill';
import { fetchEventSource } from '@microsoft/fetch-event-source';
/**
* API to talk to `techdocs-backend`.
@@ -124,18 +124,17 @@ export class TechDocsClient implements TechDocsApi {
export class TechDocsStorageClient implements TechDocsStorageApi {
public configApi: Config;
public discoveryApi: DiscoveryApi;
public identityApi: IdentityApi;
private fetchApi: FetchApi;
constructor(options: {
configApi: Config;
discoveryApi: DiscoveryApi;
identityApi: IdentityApi;
fetchApi: FetchApi;
/** @deprecated identityApi is not needed any more */
identityApi?: IdentityApi;
}) {
this.configApi = options.configApi;
this.discoveryApi = options.discoveryApi;
this.identityApi = options.identityApi;
this.fetchApi = options.fetchApi;
}
@@ -213,47 +212,32 @@ export class TechDocsStorageClient implements TechDocsStorageApi {
const apiOrigin = await this.getApiOrigin();
const url = `${apiOrigin}/sync/${namespace}/${kind}/${name}`;
const { token } = await this.identityApi.getCredentials();
return new Promise((resolve, reject) => {
// Polyfill is used to add support for custom headers and auth
const source = new EventSourcePolyfill(url, {
withCredentials: true,
headers: token ? { Authorization: `Bearer ${token}` } : {},
});
source.addEventListener('log', (e: any) => {
if (e.data) {
logHandler(JSON.parse(e.data));
}
});
source.addEventListener('finish', (e: any) => {
let updated: boolean = false;
if (e.data) {
({ updated } = JSON.parse(e.data));
}
resolve(updated ? 'updated' : 'cached');
});
source.onerror = (e: any) => {
source.close();
switch (e.status) {
// the endpoint returned a 404 status
case 404:
reject(new NotFoundError(e.message));
return;
// also handles the event-stream close. the reject is ignored if the Promise was already
// resolved by a finish event.
default:
const ctrl = new AbortController();
fetchEventSource(url, {
fetch: this.fetchApi.fetch,
signal: ctrl.signal,
onmessage(e: any) {
if (e.event === 'log') {
if (e.data) {
logHandler(JSON.parse(e.data));
}
} else if (e.event === 'finish') {
let updated: boolean = false;
if (e.data) {
({ updated } = JSON.parse(e.data));
}
resolve(updated ? 'updated' : 'cached');
} else if (e.event === 'error') {
reject(new Error(e.data));
return;
}
};
}
},
onerror(err) {
ctrl.abort();
reject(err);
},
});
});
}
-27
View File
@@ -1,27 +0,0 @@
/*
* 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.
*/
declare module 'event-source-polyfill' {
export class EventSourcePolyfill extends EventSource {
constructor(
url: string,
options?: {
withCredentials?: boolean;
headers?: HeadersInit;
},
);
}
}
+1 -4
View File
@@ -31,7 +31,6 @@ import {
createRoutableExtension,
discoveryApiRef,
fetchApiRef,
identityApiRef,
} from '@backstage/core-plugin-api';
import {
createSearchResultListItemExtension,
@@ -52,14 +51,12 @@ export const techdocsPlugin = createPlugin({
deps: {
configApi: configApiRef,
discoveryApi: discoveryApiRef,
identityApi: identityApiRef,
fetchApi: fetchApiRef,
},
factory: ({ configApi, discoveryApi, identityApi, fetchApi }) =>
factory: ({ configApi, discoveryApi, fetchApi }) =>
new TechDocsStorageClient({
configApi,
discoveryApi,
identityApi,
fetchApi,
}),
}),
+7 -6
View File
@@ -36,11 +36,11 @@ const depTypes = [
const roleRules = [
{
sourceRole: ['frontend-plugin', 'web-library'],
targetRole: ['backend-plugin', 'node-library'],
targetRole: ['backend-plugin', 'node-library', 'backend-plugin-module'],
message: `Package SOURCE_NAME with frontend role SOURCE_ROLE has a dependency on package TARGET_NAME with backend role TARGET_ROLE, which is not permitted`,
},
{
sourceRole: ['backend-plugin', 'node-library'],
sourceRole: ['backend-plugin', 'node-library', 'backend-plugin-module'],
targetRole: ['frontend-plugin', 'web-library'],
message: `Package SOURCE_NAME with backend role SOURCE_ROLE has a dependency on package TARGET_NAME with frontend role TARGET_ROLE, which is not permitted`,
},
@@ -51,8 +51,9 @@ const roleRules = [
'web-library',
'backend-plugin',
'node-library',
'backend-plugin-module',
],
message: `Polymorphic package SOURCE_NAME has a dependency on package TARGET_NAME with role TARGET_ROLE, which is not permitted since it's not also polymorphic`,
message: `Polymorphic package SOURCE_NAME with role SOURCE_ROLE has a dependency on package TARGET_NAME with role TARGET_ROLE, which is not permitted since it's not also polymorphic`,
},
{
sourceRole: ['frontend-plugin', 'web-library'],
@@ -62,7 +63,7 @@ const roleRules = [
'@backstage/plugin-api-docs',
'@backstage/plugin-techdocs-addons-test-utils',
],
message: `Package SOURCE_NAME with role SOURCE_ROLE has a dependency on another plugin package TARGET_NAME, which is not permitted`,
message: `Package SOURCE_NAME with role SOURCE_ROLE has a dependency on another plugin package TARGET_NAME with role TARGET_ROLE, which is not permitted`,
},
{
sourceRole: ['frontend-plugin', 'web-library'],
@@ -80,7 +81,7 @@ const roleRules = [
'@backstage/plugin-techdocs-addons-test-utils',
'@backstage/plugin-user-settings',
],
message: `Plugin package SOURCE_NAME with role SOURCE_ROLE has a runtime dependency on package TARGET_NAME, which is not permitted. If you are using this dependency for dev server purposes, you can move it to devDependencies instead.`,
message: `Plugin package SOURCE_NAME with role SOURCE_ROLE has a runtime dependency on package TARGET_NAME with role TARGET_ROLE, which is not permitted. If you are using this dependency for dev server purposes, you can move it to devDependencies instead.`,
},
{
sourceRole: ['backend-plugin', 'node-library'],
@@ -92,7 +93,7 @@ const roleRules = [
'@backstage/backend-test-utils',
'@backstage/backend-dynamic-feature-service',
],
message: `Plugin package SOURCE_NAME with role SOURCE_ROLE has a runtime dependency on package TARGET_NAME, which is not permitted. If you are using this dependency for dev server purposes, you can move it to devDependencies instead.`,
message: `Plugin package SOURCE_NAME with role SOURCE_ROLE has a runtime dependency on package TARGET_NAME with role TARGET_ROLE, which is not permitted. If you are using this dependency for dev server purposes, you can move it to devDependencies instead.`,
},
];
+12 -27
View File
@@ -6979,6 +6979,7 @@ __metadata:
"@material-ui/core": ^4.12.2
"@material-ui/icons": ^4.9.1
"@material-ui/lab": 4.0.0-alpha.61
"@microsoft/fetch-event-source": ^2.0.1
"@react-hookz/web": ^24.0.0
"@rjsf/core": 5.18.2
"@rjsf/material-ui": 5.18.2
@@ -6993,7 +6994,6 @@ __metadata:
"@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0
"@uiw/react-codemirror": ^4.9.3
classnames: ^2.2.6
event-source-polyfill: ^1.0.31
git-url-parse: ^14.0.0
humanize-duration: ^3.25.1
json-schema: ^0.4.0
@@ -7557,16 +7557,15 @@ __metadata:
"@material-ui/icons": ^4.9.1
"@material-ui/lab": 4.0.0-alpha.61
"@material-ui/styles": ^4.10.0
"@microsoft/fetch-event-source": ^2.0.1
"@testing-library/dom": ^10.0.0
"@testing-library/jest-dom": ^6.0.0
"@testing-library/react": ^15.0.0
"@testing-library/user-event": ^14.0.0
"@types/dompurify": ^3.0.0
"@types/event-source-polyfill": ^1.0.0
"@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0
canvas: ^2.10.2
dompurify: ^3.0.0
event-source-polyfill: 1.0.25
git-url-parse: ^14.0.0
jss: ~10.10.0
lodash: ^4.17.21
@@ -10529,6 +10528,13 @@ __metadata:
languageName: node
linkType: hard
"@microsoft/fetch-event-source@npm:^2.0.1":
version: 2.0.1
resolution: "@microsoft/fetch-event-source@npm:2.0.1"
checksum: a50e1c0f33220206967266d0a4bbba0703e2793b079d9f6e6bfd48f71b2115964a803e14cf6e902c6fab321edc084f26022334f5eaacc2cec87f174715d41852
languageName: node
linkType: hard
"@microsoft/microsoft-graph-types@npm:^2.6.0":
version: 2.40.0
resolution: "@microsoft/microsoft-graph-types@npm:2.40.0"
@@ -14253,8 +14259,8 @@ __metadata:
linkType: hard
"@rollup/plugin-commonjs@npm:^25.0.0":
version: 25.0.7
resolution: "@rollup/plugin-commonjs@npm:25.0.7"
version: 25.0.8
resolution: "@rollup/plugin-commonjs@npm:25.0.8"
dependencies:
"@rollup/pluginutils": ^5.0.1
commondir: ^1.0.1
@@ -14267,7 +14273,7 @@ __metadata:
peerDependenciesMeta:
rollup:
optional: true
checksum: 052e11839a9edc556eda5dcc759ab816dcc57e9f0f905a1e6e14fff954eaa6b1e2d0d544f5bd18d863993c5eba43d8ac9c19d9bb53b1c3b1213f32cfc9d50b2e
checksum: dd105ee5625fbcaf832c0cf80be0aaf6a86bbd8fe99ff911f9ac4b78c79f26e9e99442b5aa0cc1136b5ddf89ec0b6c5728e5341ac04d687aef1b53063670b395
languageName: node
linkType: hard
@@ -17149,13 +17155,6 @@ __metadata:
languageName: node
linkType: hard
"@types/event-source-polyfill@npm:^1.0.0":
version: 1.0.5
resolution: "@types/event-source-polyfill@npm:1.0.5"
checksum: f506b68710162f2ade1bccbc5691b8c67e5a703e565df2bc0b7b5be2637ba838ef81ec6c10b03248fe4d054386d95a6e827c7aace6e924986c2b9985f77b55de
languageName: node
linkType: hard
"@types/expect@npm:^1.20.4":
version: 1.20.4
resolution: "@types/expect@npm:1.20.4"
@@ -25522,20 +25521,6 @@ __metadata:
languageName: node
linkType: hard
"event-source-polyfill@npm:1.0.25":
version: 1.0.25
resolution: "event-source-polyfill@npm:1.0.25"
checksum: ed30428cc80eadfd693d267ba4a72dceaae938174cd116081ce38ad62bfd95f199430be7e8341e6f8f1e29489bbd5cfd4b3f6c8d6d463435623f7f91ae5f71b1
languageName: node
linkType: hard
"event-source-polyfill@npm:^1.0.31":
version: 1.0.31
resolution: "event-source-polyfill@npm:1.0.31"
checksum: 973f226404e2a1b14ed7ef15c718b89e213b41d7cfeeb1c10937fd09229f13904f3d7c3075ab28ccf858c213007559908eecdd577577330352f53a351383dd75
languageName: node
linkType: hard
"event-target-shim@npm:^5.0.0":
version: 5.0.1
resolution: "event-target-shim@npm:5.0.1"