From 9457319d265d12852691e8bae250b7ff157a6870 Mon Sep 17 00:00:00 2001 From: KaemonIsland Date: Mon, 7 Oct 2024 16:57:21 -0400 Subject: [PATCH 001/106] Add region param to the getDefaultCredentialsChain Signed-off-by: KaemonIsland --- .../src/DefaultAwsCredentialsManager.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/integration-aws-node/src/DefaultAwsCredentialsManager.ts b/packages/integration-aws-node/src/DefaultAwsCredentialsManager.ts index 12e1e2dffd..2b7ae3e1e5 100644 --- a/packages/integration-aws-node/src/DefaultAwsCredentialsManager.ts +++ b/packages/integration-aws-node/src/DefaultAwsCredentialsManager.ts @@ -77,8 +77,15 @@ function getProfileCredentials( }); } -function getDefaultCredentialsChain(): AwsCredentialIdentityProvider { - return fromNodeProviderChain(); +/** + * Include the region if present, otherwise use the default region + * + * @see https://www.npmjs.com/package/@aws-sdk/credential-provider-node + */ +function getDefaultCredentialsChain( + region = 'us-east-1', +): AwsCredentialIdentityProvider { + return fromNodeProviderChain({ clientConfig: { region } }); } /** @@ -123,7 +130,7 @@ function getSdkCredentialProvider( return getProfileCredentials(config.profile!, config.region); } - return getDefaultCredentialsChain(); + return getDefaultCredentialsChain(config.region); } /** @@ -145,7 +152,7 @@ function getMainAccountSdkCredentialProvider( return getProfileCredentials(config.profile!, config.region); } - return getDefaultCredentialsChain(); + return getDefaultCredentialsChain(config.region); } /** From 9e3e04d231e3d6c5f97704889da600fcba160d2a Mon Sep 17 00:00:00 2001 From: KaemonIsland Date: Tue, 8 Oct 2024 12:50:55 -0400 Subject: [PATCH 002/106] Add unit tests Signed-off-by: KaemonIsland --- .../src/DefaultAwsCredentialsManager.test.ts | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/packages/integration-aws-node/src/DefaultAwsCredentialsManager.test.ts b/packages/integration-aws-node/src/DefaultAwsCredentialsManager.test.ts index 237428c3e9..ed61f45199 100644 --- a/packages/integration-aws-node/src/DefaultAwsCredentialsManager.test.ts +++ b/packages/integration-aws-node/src/DefaultAwsCredentialsManager.test.ts @@ -24,12 +24,20 @@ import { } from '@aws-sdk/client-sts'; import { Config, ConfigReader } from '@backstage/config'; import { promises } from 'fs'; +import { fromNodeProviderChain } from '@aws-sdk/credential-providers'; const env = process.env; let stsMock: AwsClientStub; let config: Config; jest.mock('fs', () => ({ promises: { readFile: jest.fn() } })); +jest.mock('@aws-sdk/credential-providers', () => { + const originalModule = jest.requireActual('@aws-sdk/credential-providers'); + return { + ...originalModule, + fromNodeProviderChain: jest.fn(), + }; +}); describe('DefaultAwsCredentialsManager', () => { beforeEach(() => { @@ -134,6 +142,16 @@ describe('DefaultAwsCredentialsManager', () => { '2022-01-10', ).toISOString(); + // Return creds from env + (fromNodeProviderChain as jest.Mock).mockReturnValue(() => + Promise.resolve({ + accessKeyId: process.env.AWS_ACCESS_KEY_ID, + secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, + sessionToken: process.env.AWS_SESSION_TOKEN, + expiration: new Date(process.env.AWS_CREDENTIAL_EXPIRATION), + }), + ); + const mockProfile = `[my-profile] aws_access_key_id=ACCESS_KEY_ID_9 aws_secret_access_key=SECRET_ACCESS_KEY_9 @@ -431,5 +449,49 @@ describe('DefaultAwsCredentialsManager', () => { provider.getCredentialProvider({ accountId: '123456789012' }), ).rejects.toThrow(/No credentials found/); }); + + it('passes the region to getDefaultCredentialsChain', async () => { + const region = 'us-west-2'; + const configWithRegion = new ConfigReader({ + aws: { + mainAccount: { + region, + }, + }, + }); + + const provider = + DefaultAwsCredentialsManager.fromConfig(configWithRegion); + const awsCredentialProvider = await provider.getCredentialProvider(); + + // Trigger the call to fromNodeProviderChain + await awsCredentialProvider.sdkCredentialProvider(); + + expect(fromNodeProviderChain).toHaveBeenCalledWith({ + clientConfig: { + region, + }, + }); + }); + + it('uses default region when none is specified', async () => { + const configWithoutRegion = new ConfigReader({ + aws: { + mainAccount: {}, + }, + }); + + const provider = + DefaultAwsCredentialsManager.fromConfig(configWithoutRegion); + const awsCredentialProvider = await provider.getCredentialProvider(); + + await awsCredentialProvider.sdkCredentialProvider(); + + expect(fromNodeProviderChain).toHaveBeenCalledWith({ + clientConfig: { + region: 'us-east-1', + }, + }); + }); }); }); From 52ae92d52532da504e043572a7c21d6faf754a18 Mon Sep 17 00:00:00 2001 From: KaemonIsland Date: Tue, 8 Oct 2024 13:18:26 -0400 Subject: [PATCH 003/106] Add changeset Signed-off-by: KaemonIsland --- .changeset/friendly-hats-push.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/friendly-hats-push.md diff --git a/.changeset/friendly-hats-push.md b/.changeset/friendly-hats-push.md new file mode 100644 index 0000000000..e730fcbf8c --- /dev/null +++ b/.changeset/friendly-hats-push.md @@ -0,0 +1,5 @@ +--- +'@backstage/integration-aws-node': patch +--- + +The `getDefaultCredentialsChain` function now accepts and applies a `region` parameter, preventing it from defaulting to `us-east-1` when no region is specified. From aa21f6ef6c809544872220b906ed97380642c997 Mon Sep 17 00:00:00 2001 From: KaemonIsland Date: Tue, 8 Oct 2024 13:44:15 -0400 Subject: [PATCH 004/106] Update unit tests Signed-off-by: KaemonIsland --- .../src/DefaultAwsCredentialsManager.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/integration-aws-node/src/DefaultAwsCredentialsManager.test.ts b/packages/integration-aws-node/src/DefaultAwsCredentialsManager.test.ts index ed61f45199..4f57cb2db1 100644 --- a/packages/integration-aws-node/src/DefaultAwsCredentialsManager.test.ts +++ b/packages/integration-aws-node/src/DefaultAwsCredentialsManager.test.ts @@ -135,12 +135,12 @@ describe('DefaultAwsCredentialsManager', () => { }, }); + const testDate = new Date('2022-01-10'); + process.env.AWS_ACCESS_KEY_ID = 'ACCESS_KEY_ID_10'; process.env.AWS_SECRET_ACCESS_KEY = 'SECRET_ACCESS_KEY_10'; process.env.AWS_SESSION_TOKEN = 'SESSION_TOKEN_10'; - process.env.AWS_CREDENTIAL_EXPIRATION = new Date( - '2022-01-10', - ).toISOString(); + process.env.AWS_CREDENTIAL_EXPIRATION = testDate.toISOString(); // Return creds from env (fromNodeProviderChain as jest.Mock).mockReturnValue(() => @@ -148,7 +148,7 @@ describe('DefaultAwsCredentialsManager', () => { accessKeyId: process.env.AWS_ACCESS_KEY_ID, secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, sessionToken: process.env.AWS_SESSION_TOKEN, - expiration: new Date(process.env.AWS_CREDENTIAL_EXPIRATION), + expiration: testDate, }), ); From 45221d9ea3e566f19da69832da0d0de25a31c515 Mon Sep 17 00:00:00 2001 From: KaemonIsland Date: Mon, 14 Oct 2024 15:21:08 -0400 Subject: [PATCH 005/106] Update unit tests for correct mocking Signed-off-by: KaemonIsland --- .../src/DefaultAwsCredentialsManager.test.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/packages/integration-aws-node/src/DefaultAwsCredentialsManager.test.ts b/packages/integration-aws-node/src/DefaultAwsCredentialsManager.test.ts index 4f57cb2db1..d81f717a58 100644 --- a/packages/integration-aws-node/src/DefaultAwsCredentialsManager.test.ts +++ b/packages/integration-aws-node/src/DefaultAwsCredentialsManager.test.ts @@ -143,13 +143,8 @@ describe('DefaultAwsCredentialsManager', () => { process.env.AWS_CREDENTIAL_EXPIRATION = testDate.toISOString(); // Return creds from env - (fromNodeProviderChain as jest.Mock).mockReturnValue(() => - Promise.resolve({ - accessKeyId: process.env.AWS_ACCESS_KEY_ID, - secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, - sessionToken: process.env.AWS_SESSION_TOKEN, - expiration: testDate, - }), + (fromNodeProviderChain as jest.Mock).mockImplementation( + jest.requireActual('@aws-sdk/credential-providers').fromNodeProviderChain, ); const mockProfile = `[my-profile] From 9790c02d16e986fc70f0d66daf20420a7b4f1691 Mon Sep 17 00:00:00 2001 From: Rogerio Angeliski Date: Wed, 16 Oct 2024 11:48:56 -0300 Subject: [PATCH 006/106] fix(catalog-backend-module-github): update parent to not send a object with empty value fix #26109 Signed-off-by: Rogerio Angeliski --- .changeset/rotten-mangos-hug.md | 5 ++ .../providers/GithubOrgEntityProvider.test.ts | 89 +++++++++++++++++++ .../src/providers/GithubOrgEntityProvider.ts | 4 +- 3 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 .changeset/rotten-mangos-hug.md diff --git a/.changeset/rotten-mangos-hug.md b/.changeset/rotten-mangos-hug.md new file mode 100644 index 0000000000..013a815653 --- /dev/null +++ b/.changeset/rotten-mangos-hug.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-github': patch +--- + +Fix bug when receive a `team.creted` github event without parent diff --git a/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.test.ts b/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.test.ts index 82c93b0d57..52dc2b6d14 100644 --- a/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.test.ts +++ b/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.test.ts @@ -530,6 +530,95 @@ describe('GithubOrgEntityProvider', () => { }); }); + it('should apply delta added on receive a created team without parent', async () => { + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + + const logger = mockServices.logger.mock(); + const events = DefaultEventsService.create({ logger }); + const gitHubConfig: GithubIntegrationConfig = { + host: 'github.com', + }; + + const mockGetCredentials = jest.fn().mockReturnValue({ + headers: { token: 'blah' }, + type: 'app', + }); + + const githubCredentialsProvider: GithubCredentialsProvider = { + getCredentials: mockGetCredentials, + }; + + const entityProvider = new GithubOrgEntityProvider({ + events, + id: 'my-id', + githubCredentialsProvider, + orgUrl: 'https://github.com/backstage', + gitHubConfig, + logger, + }); + + entityProvider.connect(entityProviderConnection); + + const expectedEntity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + name: 'new-team', + description: 'description from the new team', + annotations: { + 'backstage.io/edit-url': + 'https://github.com/orgs/test-org/teams/new-team/edit', + 'backstage.io/managed-by-location': + 'url:https://github.com/orgs/test-org/teams/new-team', + 'backstage.io/managed-by-origin-location': + 'url:https://github.com/orgs/test-org/teams/new-team', + 'github.com/team-slug': 'test-org/new-team', + }, + }, + spec: { + type: 'team', + children: [], + members: [], + profile: { + displayName: 'New Team', + }, + }, + }; + + const event: EventParams = { + topic: 'github.team', + eventPayload: { + action: 'created', + team: { + name: 'New Team', + slug: 'new-team', + description: 'description from the new team', + html_url: 'https://github.com/orgs/test-org/teams/new-team', + }, + organization: { + login: 'test-org', + }, + }, + }; + + await events.publish(event); + + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1); + expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({ + type: 'delta', + added: [ + { + locationKey: 'github-org-provider:my-id', + entity: expectedEntity, + }, + ], + removed: [], + }); + }); + it('should apply delta removed on receive a deleted team', async () => { const entityProviderConnection: EntityProviderConnection = { applyMutation: jest.fn(), diff --git a/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.ts b/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.ts index f7183b4df9..6e83b8d65f 100644 --- a/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.ts +++ b/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.ts @@ -511,7 +511,9 @@ export class GithubOrgEntityProvider implements EntityProvider { editTeamUrl: `${url}/edit`, combinedSlug: `${org}/${slug}`, description: description || undefined, - parentTeam: { slug: event.team?.parent?.slug || '' } as GithubTeam, + parentTeam: event.team?.parent?.slug + ? ({ slug: event.team.parent.slug } as GithubTeam) + : undefined, // entity will be removed members: [], }, From c19f109d9e79801df258aa2c49c418fe0ffaf771 Mon Sep 17 00:00:00 2001 From: KaemonIsland Date: Thu, 17 Oct 2024 10:52:49 -0400 Subject: [PATCH 007/106] Update comment Signed-off-by: KaemonIsland --- .../integration-aws-node/src/DefaultAwsCredentialsManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/integration-aws-node/src/DefaultAwsCredentialsManager.ts b/packages/integration-aws-node/src/DefaultAwsCredentialsManager.ts index 2b7ae3e1e5..3048fdfe40 100644 --- a/packages/integration-aws-node/src/DefaultAwsCredentialsManager.ts +++ b/packages/integration-aws-node/src/DefaultAwsCredentialsManager.ts @@ -78,7 +78,7 @@ function getProfileCredentials( } /** - * Include the region if present, otherwise use the default region + * Include the region if present, otherwise use the default region. * * @see https://www.npmjs.com/package/@aws-sdk/credential-provider-node */ From 8bb8c302ae007a7fb4a92d50a9320313edb929c6 Mon Sep 17 00:00:00 2001 From: Rogerio Angeliski Date: Fri, 18 Oct 2024 11:53:18 -0300 Subject: [PATCH 008/106] Update .changeset/rotten-mangos-hug.md Co-authored-by: Vincenzo Scamporlino Signed-off-by: Rogerio Angeliski --- .changeset/rotten-mangos-hug.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/rotten-mangos-hug.md b/.changeset/rotten-mangos-hug.md index 013a815653..022d3f3d45 100644 --- a/.changeset/rotten-mangos-hug.md +++ b/.changeset/rotten-mangos-hug.md @@ -2,4 +2,4 @@ '@backstage/plugin-catalog-backend-module-github': patch --- -Fix bug when receive a `team.creted` github event without parent +Fixed an issue in `GithubOrgEntityProvider` that caused an error when processing teams without a parent. From b533056c81cd355561119dbd483668b85c09dfd6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 24 Oct 2024 06:05:26 +0000 Subject: [PATCH 009/106] fix(deps): update dependency css-loader to v7 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .changeset/renovate-6193787.md | 5 +++++ packages/cli/package.json | 2 +- yarn.lock | 12 ++++++------ 3 files changed, 12 insertions(+), 7 deletions(-) create mode 100644 .changeset/renovate-6193787.md diff --git a/.changeset/renovate-6193787.md b/.changeset/renovate-6193787.md new file mode 100644 index 0000000000..ce392b7c81 --- /dev/null +++ b/.changeset/renovate-6193787.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Updated dependency `css-loader` to `^7.0.0`. diff --git a/packages/cli/package.json b/packages/cli/package.json index de183b4d15..278c0aec3e 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -88,7 +88,7 @@ "commander": "^12.0.0", "cross-fetch": "^4.0.0", "cross-spawn": "^7.0.3", - "css-loader": "^6.5.1", + "css-loader": "^7.0.0", "ctrlc-windows": "^2.1.0", "esbuild": "^0.24.0", "esbuild-loader": "^4.0.0", diff --git a/yarn.lock b/yarn.lock index 22aaacbd73..b22ada9924 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3992,7 +3992,7 @@ __metadata: commander: ^12.0.0 cross-fetch: ^4.0.0 cross-spawn: ^7.0.3 - css-loader: ^6.5.1 + css-loader: ^7.0.0 ctrlc-windows: ^2.1.0 del: ^8.0.0 esbuild: ^0.24.0 @@ -24483,9 +24483,9 @@ __metadata: languageName: node linkType: hard -"css-loader@npm:^6.5.1": - version: 6.11.0 - resolution: "css-loader@npm:6.11.0" +"css-loader@npm:^7.0.0": + version: 7.1.2 + resolution: "css-loader@npm:7.1.2" dependencies: icss-utils: ^5.1.0 postcss: ^8.4.33 @@ -24497,13 +24497,13 @@ __metadata: semver: ^7.5.4 peerDependencies: "@rspack/core": 0.x || 1.x - webpack: ^5.0.0 + webpack: ^5.27.0 peerDependenciesMeta: "@rspack/core": optional: true webpack: optional: true - checksum: 5c8d35975a7121334905394e88e28f05df72f037dbed2fb8fec4be5f0b313ae73a13894ba791867d4a4190c35896da84a7fd0c54fb426db55d85ba5e714edbe3 + checksum: 15bfd90d778ddab90ee1d04c8c8bcc13ea6c0791d01b52b09d1b1c753b3410f7a7788a510d93726a9878e70b7c1a140f21efdf5c96e1857872107551d3897822 languageName: node linkType: hard From 50df3c87eb1598efc1376eba22a160405c303497 Mon Sep 17 00:00:00 2001 From: Yash Oswal Date: Tue, 29 Oct 2024 11:13:10 +0530 Subject: [PATCH 010/106] feat(catalog): Implement breadcrumbs for entity navigation (#26898) updadted tests Signed-off-by: Yash Oswal --- .changeset/afraid-carrots-greet.md | 7 ++ .../components/catalog/EntityPage.test.tsx | 4 +- .../app/src/components/catalog/EntityPage.tsx | 17 ++-- plugins/catalog/report.api.md | 1 + .../EntityLayout/EntityLayout.test.tsx | 32 +++++++ .../components/EntityLayout/EntityLayout.tsx | 92 ++++++++++++++++++- 6 files changed, 139 insertions(+), 14 deletions(-) create mode 100644 .changeset/afraid-carrots-greet.md diff --git a/.changeset/afraid-carrots-greet.md b/.changeset/afraid-carrots-greet.md new file mode 100644 index 0000000000..9f77cba9b8 --- /dev/null +++ b/.changeset/afraid-carrots-greet.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-catalog': minor +--- + +- Updated EntityLayout component to implement breadcrumb navigation based on the entity relations. + +- Added parentEntityRelations prop to EntityLayoutProps to specify relation types for parent entities. diff --git a/packages/app/src/components/catalog/EntityPage.test.tsx b/packages/app/src/components/catalog/EntityPage.test.tsx index 0943a82efb..06a568279b 100644 --- a/packages/app/src/components/catalog/EntityPage.test.tsx +++ b/packages/app/src/components/catalog/EntityPage.test.tsx @@ -13,12 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - import { EntityLayout, catalogPlugin } from '@backstage/plugin-catalog'; import { EntityProvider, starredEntitiesApiRef, MockStarredEntitiesApi, + catalogApiRef, } from '@backstage/plugin-catalog-react'; import { permissionApiRef } from '@backstage/plugin-permission-react'; import { @@ -28,6 +28,7 @@ import { } from '@backstage/test-utils'; import React from 'react'; import { cicdContent } from './EntityPage'; +import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils'; describe('EntityPage Test', () => { const entity = { @@ -55,6 +56,7 @@ describe('EntityPage Test', () => { apis={[ [starredEntitiesApiRef, new MockStarredEntitiesApi()], [permissionApiRef, mockApis.permission()], + [catalogApiRef, catalogApiMock()], ]} > diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx index fe5a7e0ace..11fc60bd0e 100644 --- a/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/app/src/components/catalog/EntityPage.tsx @@ -85,15 +85,14 @@ const customEntityFilterKind = ['Component', 'API', 'System']; const EntityLayoutWrapper = (props: { children?: ReactNode }) => { return ( - <> - - {props.children} - - + + {props.children} + ); }; diff --git a/plugins/catalog/report.api.md b/plugins/catalog/report.api.md index d817118e3a..ffb46afc6e 100644 --- a/plugins/catalog/report.api.md +++ b/plugins/catalog/report.api.md @@ -414,6 +414,7 @@ export interface EntityLayoutProps { children?: React_2.ReactNode; // (undocumented) NotFoundComponent?: React_2.ReactNode; + parentEntityRelations?: string[]; // Warning: (ae-forgotten-export) The symbol "EntityContextMenuOptions" needs to be exported by the entry point index.d.ts // // (undocumented) diff --git a/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx b/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx index 239b081051..ff38e53d96 100644 --- a/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx +++ b/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx @@ -169,6 +169,38 @@ describe('EntityLayout', () => { expect(screen.queryByText('tabbed-test-content')).not.toBeInTheDocument(); }); + it('renders the breadcrumbs if defined', async () => { + const mockEntityWithRelation = { + kind: 'MyKind', + metadata: { + name: 'my-entity', + namespace: 'default', + title: 'My Entity', + }, + relations: [{ type: 'partOf', targetRef: 'system:default/my-system' }], + } as Entity; + + await renderInTestApp( + + + + +
tabbed-test-content
+
+
+
+
, + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, + '/catalog': rootRouteRef, + }, + }, + ); + + expect(screen.getByText('my-system')).toBeInTheDocument(); + }); + it('navigates when user clicks different tab', async () => { await renderInTestApp( diff --git a/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx b/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx index e8b957497d..c06344c2a6 100644 --- a/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx +++ b/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx @@ -15,11 +15,13 @@ */ import { - Entity, DEFAULT_NAMESPACE, + Entity, + EntityRelation, RELATION_OWNED_BY, } from '@backstage/catalog-model'; import { + Breadcrumbs, Content, Header, HeaderLabel, @@ -32,12 +34,16 @@ import { import { attachComponentData, IconComponent, + useApi, useElementFilter, useRouteRef, useRouteRefParams, } from '@backstage/core-plugin-api'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; import { + catalogApiRef, EntityDisplayName, + EntityRefLink, EntityRefLinks, entityRouteRef, FavoriteEntity, @@ -47,14 +53,15 @@ import { useAsyncEntity, } from '@backstage/plugin-catalog-react'; import Box from '@material-ui/core/Box'; +import { makeStyles } from '@material-ui/core/styles'; import { TabProps } from '@material-ui/core/Tab'; import Alert from '@material-ui/lab/Alert'; import React, { useEffect, useState } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; -import { EntityContextMenu } from '../EntityContextMenu/EntityContextMenu'; -import { rootRouteRef, unregisterRedirectRouteRef } from '../../routes'; +import useAsync from 'react-use/esm/useAsync'; import { catalogTranslationRef } from '../../alpha/translation'; -import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; +import { rootRouteRef, unregisterRedirectRouteRef } from '../../routes'; +import { EntityContextMenu } from '../EntityContextMenu/EntityContextMenu'; /** @public */ export type EntityLayoutRouteProps = { @@ -101,6 +108,7 @@ function headerProps( const namespace = paramNamespace ?? entity?.metadata.namespace ?? ''; const name = entity?.metadata.title ?? paramName ?? entity?.metadata.name ?? ''; + return { headerTitle: `${name}${ namespace && namespace !== DEFAULT_NAMESPACE ? ` in ${namespace}` : '' @@ -167,8 +175,49 @@ export interface EntityLayoutProps { UNSTABLE_contextMenuOptions?: EntityContextMenuOptions; children?: React.ReactNode; NotFoundComponent?: React.ReactNode; + /** + * An array of relation types used to determine the parent entities in the hierarchy. + * These relations are prioritized in the order provided, allowing for flexible + * navigation through entity relationships. + * + * For example, use relation types like `["partOf", "memberOf", "ownedBy"]` to define how the entity is related to + * its parents in the Entity Catalog. + * + * It adds breadcrumbs in the Entity page to enhance user navigation and context awareness. + */ + parentEntityRelations?: string[]; } +function findParentRelation( + entityRelations: EntityRelation[] = [], + relationTypes: string[] = [], +) { + for (const type of relationTypes) { + const foundRelation = entityRelations.find( + relation => relation.type === type, + ); + if (foundRelation) { + return foundRelation; // Return the first found relation and stop + } + } + return null; +} + +const useStyles = makeStyles(theme => ({ + breadcrumbs: { + color: theme.page.fontColor, + fontSize: theme.typography.caption.fontSize, + textTransform: 'uppercase', + marginTop: theme.spacing(1), + opacity: 0.8, + '& span ': { + color: theme.page.fontColor, + textDecoration: 'underline', + textUnderlineOffset: '3px', + }, + }, +})); + /** * EntityLayout is a compound component, which allows you to define a layout for * entities using a sub-navigation mechanism. @@ -192,7 +241,9 @@ export const EntityLayout = (props: EntityLayoutProps) => { UNSTABLE_contextMenuOptions, children, NotFoundComponent, + parentEntityRelations, } = props; + const classes = useStyles(); const { kind, namespace, name } = useRouteRefParams(entityRouteRef); const { entity, loading, error } = useAsyncEntity(); const location = useLocation(); @@ -247,6 +298,22 @@ export const EntityLayout = (props: EntityLayoutProps) => { ); }; + const parentEntity = findParentRelation( + entity?.relations ?? [], + parentEntityRelations ?? [], + ); + + const catalogApi = useApi(catalogApiRef); + const { value: ancestorEntity } = useAsync(async () => { + if (parentEntity) { + return findParentRelation( + (await catalogApi.getEntityByRef(parentEntity?.targetRef))?.relations, + parentEntityRelations, + ); + } + return null; + }, [parentEntity]); + // Make sure to close the dialog if the user clicks links in it that navigate // to another entity. useEffect(() => { @@ -261,6 +328,23 @@ export const EntityLayout = (props: EntityLayoutProps) => { title={} pageTitleOverride={headerTitle} type={headerType} + subtitle={ + parentEntity && ( + + {ancestorEntity && ( + + )} + + {name} + + ) + } > {entity && ( <> From f32a2d3198debb7a0e5b19dde3c494711e4abf7f Mon Sep 17 00:00:00 2001 From: Paulo Eduardo Peixoto Date: Fri, 1 Nov 2024 11:19:20 -0300 Subject: [PATCH 011/106] docs(docs/features/software-templates/writing-custom-field-extensions.md): add pending imports. Signed-off-by: Paulo Eduardo Peixoto --- .../software-templates/writing-custom-field-extensions.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/features/software-templates/writing-custom-field-extensions.md b/docs/features/software-templates/writing-custom-field-extensions.md index feb0f68048..7f6e4b8796 100644 --- a/docs/features/software-templates/writing-custom-field-extensions.md +++ b/docs/features/software-templates/writing-custom-field-extensions.md @@ -33,6 +33,12 @@ import React from 'react'; import { FieldExtensionComponentProps } from '@backstage/plugin-scaffolder-react'; import type { FieldValidation } from '@rjsf/utils'; import FormControl from '@material-ui/core/FormControl'; +import { + FormControl, + FormHelperText, + Input, + InputLabel, +} from '@material-ui/core'; /* This is the actual component that will get rendered in the form */ From b89834bfa65c48bca616e8894cb1ad55faf0e26f Mon Sep 17 00:00:00 2001 From: Jordan Slott Date: Thu, 31 Oct 2024 17:03:55 -0400 Subject: [PATCH 012/106] Fixes #27325 Stitch entity for which target of relationship has changed Signed-off-by: Jordan Slott --- .changeset/short-pots-remember.md | 5 + .../DefaultCatalogProcessingEngine.test.ts | 105 ++++++++++++++++++ .../DefaultCatalogProcessingEngine.ts | 8 +- 3 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 .changeset/short-pots-remember.md diff --git a/.changeset/short-pots-remember.md b/.changeset/short-pots-remember.md new file mode 100644 index 0000000000..9a30bc222c --- /dev/null +++ b/.changeset/short-pots-remember.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Fixed an issue where entities would not be marked for restitching if only the target of a relationship changed. diff --git a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.test.ts b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.test.ts index 3f283a5b38..de7c3600c2 100644 --- a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.test.ts +++ b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.test.ts @@ -464,6 +464,111 @@ describe('DefaultCatalogProcessingEngine', () => { await engine.stop(); }); + it('should stitch both the previous and new sources when relation target changes', async () => { + const engine = new DefaultCatalogProcessingEngine({ + config: new ConfigReader({}), + logger: mockServices.logger.mock(), + processingDatabase: db, + knex: {} as any, + orchestrator: orchestrator, + stitcher: stitcher, + createHash: () => hash, + pollingIntervalMs: 100, + }); + + db.transaction.mockImplementation(cb => cb((() => {}) as any)); + + const entity = { + apiVersion: '1', + kind: 'k', + metadata: { name: 'me', namespace: 'ns' }, + }; + const processableEntity = { + entityRef: 'foo', + id: '1', + unprocessedEntity: entity, + resultHash: '', + state: [] as any, + nextUpdateAt: DateTime.now(), + lastDiscoveryAt: DateTime.now(), + }; + + db.listParents.mockResolvedValue({ entityRefs: [] }); + db.getProcessableEntities + .mockResolvedValueOnce({ + items: [processableEntity], + }) + .mockResolvedValueOnce({ + items: [processableEntity], + }); + db.updateProcessedEntity + .mockImplementationOnce(async () => ({ + previous: { relations: [] }, + })) + .mockImplementationOnce(async () => ({ + previous: { + relations: [ + { + originating_entity_id: '', + type: 't', + source_entity_ref: 'k:ns/other1', + target_entity_ref: 'k:ns/me', + }, + ], + }, + })); + + orchestrator.process + .mockResolvedValueOnce({ + ok: true, + completedEntity: entity, + relations: [ + { + type: 't', + source: { kind: 'k', namespace: 'ns', name: 'other1' }, + target: { kind: 'k', namespace: 'ns', name: 'me' }, + }, + ], + errors: [], + deferredEntities: [], + state: {}, + refreshKeys: [], + }) + .mockResolvedValueOnce({ + ok: true, + completedEntity: entity, + // change just the target of the relationship to a new entity, + // leaving the source and relation type the same. + // see: https://github.com/backstage/backstage/issues/27325 + relations: [ + { + type: 't', + source: { kind: 'k', namespace: 'ns', name: 'other1' }, + target: { kind: 'k', namespace: 'ns', name: 'newtarget' }, + }, + ], + errors: [], + deferredEntities: [], + state: {}, + refreshKeys: [], + }); + + await engine.start(); + await waitForExpect(() => { + expect(stitcher.stitch).toHaveBeenCalledTimes(2); + }); + expect([...stitcher.stitch.mock.calls[0][0].entityRefs!]).toEqual( + expect.arrayContaining(['k:ns/me', 'k:ns/other1']), + ); + // As a result of switching the relationship for source other1 to + // a new target entity, the other1 relationship source must be + // restitched. + expect([...stitcher.stitch.mock.calls[1][0].entityRefs!]).toEqual( + expect.arrayContaining(['k:ns/me', 'k:ns/other1']), + ); + await engine.stop(); + }); + it('should not stitch sources entities when relations are the same', async () => { const engine = new DefaultCatalogProcessingEngine({ config: new ConfigReader({}), diff --git a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.ts b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.ts index e42847bafe..0f7acba652 100644 --- a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.ts +++ b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.ts @@ -295,7 +295,7 @@ export class DefaultCatalogProcessingEngine { }); oldRelationSources = new Map( previous.relations.map(r => [ - `${r.source_entity_ref}:${r.type}`, + `${r.source_entity_ref}:${r.type}->${r.target_entity_ref}`, r.source_entity_ref, ]), ); @@ -304,7 +304,11 @@ export class DefaultCatalogProcessingEngine { const newRelationSources = new Map( result.relations.map(relation => { const sourceEntityRef = stringifyEntityRef(relation.source); - return [`${sourceEntityRef}:${relation.type}`, sourceEntityRef]; + const targetEntityRef = stringifyEntityRef(relation.target); + return [ + `${sourceEntityRef}:${relation.type}->${targetEntityRef}`, + sourceEntityRef, + ]; }), ); From 1b23511acdc041d3a38516e6cf150c9d245e887e Mon Sep 17 00:00:00 2001 From: Paulo Eduardo Peixoto Date: Mon, 4 Nov 2024 08:39:21 -0300 Subject: [PATCH 013/106] docs(docs/features/software-templates/writing-custom-field-extensions.md): remove import from "@material-ui/core/FormControl". Signed-off-by: Paulo Eduardo Peixoto --- .../software-templates/writing-custom-field-extensions.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/features/software-templates/writing-custom-field-extensions.md b/docs/features/software-templates/writing-custom-field-extensions.md index 7f6e4b8796..7431b91e64 100644 --- a/docs/features/software-templates/writing-custom-field-extensions.md +++ b/docs/features/software-templates/writing-custom-field-extensions.md @@ -32,7 +32,6 @@ As an example, we will create a component that validates whether a string is in import React from 'react'; import { FieldExtensionComponentProps } from '@backstage/plugin-scaffolder-react'; import type { FieldValidation } from '@rjsf/utils'; -import FormControl from '@material-ui/core/FormControl'; import { FormControl, FormHelperText, From 6836522a8e936594c33073cacf54594d72f28759 Mon Sep 17 00:00:00 2001 From: luccas Date: Mon, 4 Nov 2024 23:25:29 -0300 Subject: [PATCH 014/106] added pagination to defaultApiExplorerPage Signed-off-by: luccas --- .changeset/angry-bags-compete.md | 5 +++++ .../components/ApiExplorerPage/DefaultApiExplorerPage.tsx | 5 ++++- 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 .changeset/angry-bags-compete.md diff --git a/.changeset/angry-bags-compete.md b/.changeset/angry-bags-compete.md new file mode 100644 index 0000000000..87e9bf36b8 --- /dev/null +++ b/.changeset/angry-bags-compete.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-api-docs': minor +--- + +Added support for pagination in api-docs plugin - DefaultApiExplorerPage diff --git a/plugins/api-docs/src/components/ApiExplorerPage/DefaultApiExplorerPage.tsx b/plugins/api-docs/src/components/ApiExplorerPage/DefaultApiExplorerPage.tsx index 6b5917c52d..b5a02b2f9e 100644 --- a/plugins/api-docs/src/components/ApiExplorerPage/DefaultApiExplorerPage.tsx +++ b/plugins/api-docs/src/components/ApiExplorerPage/DefaultApiExplorerPage.tsx @@ -29,6 +29,7 @@ import { EntityKindPicker, EntityLifecyclePicker, EntityListProvider, + EntityListPagination, EntityOwnerPicker, EntityTagPicker, EntityTypePicker, @@ -62,6 +63,7 @@ export type DefaultApiExplorerPageProps = { columns?: TableColumn[]; actions?: TableProps['actions']; ownerPickerMode?: EntityOwnerPickerProps['mode']; + pagination?: EntityListPagination; }; /** @@ -74,6 +76,7 @@ export const DefaultApiExplorerPage = (props: DefaultApiExplorerPageProps) => { columns, actions, ownerPickerMode, + pagination, } = props; const configApi = useApi(configApiRef); @@ -102,7 +105,7 @@ export const DefaultApiExplorerPage = (props: DefaultApiExplorerPageProps) => { )} All your APIs - +