github-pull-request-board decouple board from entity page (#4710)
* Decouple entities from the board logic for reuse the board on other places Signed-off-by: Juan Pablo Garcia Ripa <sarabadu@gmail.com> * mock useEntity Signed-off-by: Juan Pablo Garcia Ripa <sarabadu@gmail.com> * revert the change Signed-off-by: Juan Pablo Garcia Ripa <sarabadu@gmail.com> Signed-off-by: Juan Pablo Garcia Ripa <sarabadu@gmail.com> --------- Signed-off-by: Juan Pablo Garcia Ripa <sarabadu@gmail.com>
This commit is contained in:
parent
56382c80de
commit
c2b33a16aa
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'@backstage-community/plugin-github-pull-requests-board': patch
|
||||
---
|
||||
|
||||
Decouple entities from the board logic for reuse the board on other places
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
app:
|
||||
title: Example App
|
||||
baseUrl: http://localhost:3000
|
||||
|
||||
backend:
|
||||
baseUrl: http://localhost:7007
|
||||
|
|
@ -6,6 +6,7 @@
|
|||
"node": "18 || 20 || 22"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "backstage-cli repo start",
|
||||
"tsc": "tsc",
|
||||
"tsc:full": "tsc --skipLibCheck false --incremental false",
|
||||
"build:all": "backstage-cli repo build --all",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,194 @@
|
|||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { createDevApp } from '@backstage/dev-utils';
|
||||
import { catalogApiRef, EntityProvider } from '@backstage/plugin-catalog-react';
|
||||
import { Content, Header, HeaderLabel, Page } from '@backstage/core-components';
|
||||
import { TestApiProvider } from '@backstage/test-utils';
|
||||
import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils';
|
||||
|
||||
import Wifi from '@material-ui/icons/WifiSharp';
|
||||
import OfflineIcon from '@material-ui/icons/WifiOff';
|
||||
|
||||
import { EntityTeamPullRequestsContent } from '../src';
|
||||
import { githubAuthApiRef } from '@backstage/frontend-plugin-api';
|
||||
import React from 'react';
|
||||
|
||||
const GITHUB_PULL_REQUESTS_ANNOTATION = 'github.com/project-slug';
|
||||
const GITHUB_USER_LOGIN_ANNOTATION = 'github.com/user-login';
|
||||
|
||||
const teamEntity: Entity = {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Group',
|
||||
metadata: {
|
||||
name: 'engineering',
|
||||
},
|
||||
spec: {
|
||||
type: 'team',
|
||||
},
|
||||
relations: [
|
||||
{
|
||||
type: 'hasMember',
|
||||
targetRef: 'user:default/user1',
|
||||
},
|
||||
{
|
||||
type: 'hasMember',
|
||||
targetRef: 'user:default/user2',
|
||||
},
|
||||
],
|
||||
};
|
||||
const teamMembers: Entity[] = [
|
||||
{
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'User',
|
||||
metadata: {
|
||||
name: 'user1',
|
||||
annotations: {
|
||||
[GITHUB_USER_LOGIN_ANNOTATION]: 'Sarabadu',
|
||||
},
|
||||
},
|
||||
relations: [
|
||||
{
|
||||
type: 'memberOf',
|
||||
targetRef: 'group:default/engineering',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'User',
|
||||
metadata: {
|
||||
name: 'user2',
|
||||
annotations: {
|
||||
[GITHUB_USER_LOGIN_ANNOTATION]: 'awanlin',
|
||||
},
|
||||
},
|
||||
relations: [
|
||||
{
|
||||
type: 'memberOf',
|
||||
targetRef: 'group:default/engineering',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const components: Entity[] = [
|
||||
{
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'repo1',
|
||||
annotations: {
|
||||
[GITHUB_PULL_REQUESTS_ANNOTATION]: 'backstage/community-plugins',
|
||||
},
|
||||
},
|
||||
spec: {
|
||||
type: 'service',
|
||||
lifecycle: 'production',
|
||||
owner: 'group:engineering',
|
||||
},
|
||||
relations: [
|
||||
{
|
||||
type: 'ownedBy',
|
||||
targetRef: 'group:default/engineering',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'repo2',
|
||||
annotations: {
|
||||
[GITHUB_PULL_REQUESTS_ANNOTATION]: 'backstage/backstage',
|
||||
},
|
||||
},
|
||||
spec: {
|
||||
type: 'service',
|
||||
lifecycle: 'production',
|
||||
owner: 'group:engineering',
|
||||
},
|
||||
relations: [
|
||||
{
|
||||
type: 'ownedBy',
|
||||
targetRef: 'group:default/engineering',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
const catalogApiMockImp = catalogApiMock({
|
||||
entities: [teamEntity, ...teamMembers, ...components],
|
||||
});
|
||||
|
||||
const githubMockApi = {
|
||||
getAccessToken: async () => {
|
||||
// This is only here to make been able to locally test this plugin without having to
|
||||
// setup a backend app
|
||||
return 'mocked-token';
|
||||
},
|
||||
} as typeof githubAuthApiRef.T;
|
||||
|
||||
createDevApp()
|
||||
// .registerPlugin(githubPullRequestsBoardPlugin)
|
||||
.addPage({
|
||||
element: (
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[catalogApiRef, catalogApiMockImp],
|
||||
[githubAuthApiRef, githubMockApi],
|
||||
]}
|
||||
>
|
||||
<EntityProvider entity={teamEntity}>
|
||||
<Page themeId="service">
|
||||
<Header title="Mocked Pull Requests Board">
|
||||
<HeaderLabel label="Mode" value="Development" />
|
||||
</Header>
|
||||
<Content>
|
||||
<EntityTeamPullRequestsContent />
|
||||
</Content>
|
||||
</Page>
|
||||
</EntityProvider>
|
||||
</TestApiProvider>
|
||||
),
|
||||
title: 'Entity Todo Content',
|
||||
icon: OfflineIcon,
|
||||
})
|
||||
.addPage({
|
||||
element: (
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[catalogApiRef, catalogApiMockImp],
|
||||
[githubAuthApiRef, githubMockApi],
|
||||
]}
|
||||
>
|
||||
<EntityProvider entity={teamEntity}>
|
||||
<Page themeId="service">
|
||||
<Header title="Mocked Pull Requests Board">
|
||||
<HeaderLabel label="Mode" value="Development" />
|
||||
</Header>
|
||||
<Content>
|
||||
<EntityTeamPullRequestsContent />
|
||||
</Content>
|
||||
</Page>
|
||||
</EntityProvider>
|
||||
</TestApiProvider>
|
||||
),
|
||||
title: 'Live Pull Requests Board',
|
||||
icon: Wifi,
|
||||
})
|
||||
|
||||
.render();
|
||||
|
|
@ -72,6 +72,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.33.1",
|
||||
"@backstage/dev-utils": "^1.1.12",
|
||||
"@backstage/frontend-test-utils": "^0.3.4",
|
||||
"@backstage/test-utils": "^1.7.10",
|
||||
"@testing-library/dom": "^10.0.0",
|
||||
|
|
|
|||
|
|
@ -5,7 +5,9 @@
|
|||
```ts
|
||||
/// <reference types="react" />
|
||||
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { JSX as JSX_2 } from 'react';
|
||||
import { default as React_2 } from 'react';
|
||||
|
||||
// @public (undocumented)
|
||||
export const EntityTeamPullRequestsCard: (
|
||||
|
|
@ -29,5 +31,17 @@ export interface EntityTeamPullRequestsContentProps {
|
|||
pullRequestLimit?: number;
|
||||
}
|
||||
|
||||
// @public
|
||||
export const PullRequestsBoard: (
|
||||
props: PullRequestsBoardProps,
|
||||
) => React_2.JSX.Element;
|
||||
|
||||
// @public (undocumented)
|
||||
export interface PullRequestsBoardProps {
|
||||
entities: Entity[];
|
||||
// (undocumented)
|
||||
pullRequestLimit?: number;
|
||||
}
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
```
|
||||
|
|
|
|||
|
|
@ -32,6 +32,21 @@ jest.mock('../../hooks/useUserRepositoriesAndTeam', () => {
|
|||
};
|
||||
});
|
||||
|
||||
jest.mock('@backstage/plugin-catalog-react', () => {
|
||||
return {
|
||||
useEntity: () => {
|
||||
return {
|
||||
entity: {
|
||||
metadata: {
|
||||
name: 'test-entity',
|
||||
namespace: 'default',
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../hooks/usePullRequestsByTeam', () => {
|
||||
const buildPullRequest = ({
|
||||
prTitle,
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import { shouldDisplayCard } from '../../utils/functions';
|
|||
import { DraftPrIcon } from '../icons/DraftPr';
|
||||
import { useUserRepositoriesAndTeam } from '../../hooks/useUserRepositoriesAndTeam';
|
||||
import UnarchiveIcon from '@material-ui/icons/Unarchive';
|
||||
import { useEntity } from '@backstage/plugin-catalog-react';
|
||||
|
||||
/** @public */
|
||||
export interface EntityTeamPullRequestsCardProps {
|
||||
|
|
@ -39,12 +40,13 @@ export interface EntityTeamPullRequestsCardProps {
|
|||
const EntityTeamPullRequestsCard = (props: EntityTeamPullRequestsCardProps) => {
|
||||
const { pullRequestLimit } = props;
|
||||
const [infoCardFormat, setInfoCardFormat] = useState<PRCardFormating[]>([]);
|
||||
const { entity: teamEntity } = useEntity();
|
||||
const {
|
||||
loading: loadingReposAndTeam,
|
||||
repositories,
|
||||
teamMembers,
|
||||
teamMembersOrganization,
|
||||
} = useUserRepositoriesAndTeam();
|
||||
} = useUserRepositoriesAndTeam(teamEntity);
|
||||
const {
|
||||
loading: loadingPRs,
|
||||
pullRequests,
|
||||
|
|
|
|||
|
|
@ -32,6 +32,21 @@ jest.mock('../../hooks/useUserRepositoriesAndTeam', () => {
|
|||
};
|
||||
});
|
||||
|
||||
jest.mock('@backstage/plugin-catalog-react', () => {
|
||||
return {
|
||||
useEntity: () => {
|
||||
return {
|
||||
entity: {
|
||||
metadata: {
|
||||
name: 'test-entity',
|
||||
namespace: 'default',
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../hooks/usePullRequestsByTeam', () => {
|
||||
const buildPullRequest = ({
|
||||
prTitle,
|
||||
|
|
|
|||
|
|
@ -13,21 +13,10 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React, { useState } from 'react';
|
||||
import { Grid, Typography } from '@material-ui/core';
|
||||
import PeopleIcon from '@material-ui/icons/People';
|
||||
import { Progress, InfoCard } from '@backstage/core-components';
|
||||
|
||||
import { InfoCardHeader } from '../InfoCardHeader';
|
||||
import { PullRequestBoardOptions } from '../PullRequestBoardOptions';
|
||||
import { Wrapper } from '../Wrapper';
|
||||
import { PullRequestCard } from '../PullRequestCard';
|
||||
import { usePullRequestsByTeam } from '../../hooks/usePullRequestsByTeam';
|
||||
import { PRCardFormating } from '../../utils/types';
|
||||
import { shouldDisplayCard } from '../../utils/functions';
|
||||
import { DraftPrIcon } from '../icons/DraftPr';
|
||||
import { useUserRepositoriesAndTeam } from '../../hooks/useUserRepositoriesAndTeam';
|
||||
import UnarchiveIcon from '@material-ui/icons/Unarchive';
|
||||
import React from 'react';
|
||||
import PullRequestsBoard from '../PullRequestsBoard';
|
||||
import { useEntity } from '@backstage/plugin-catalog-react';
|
||||
|
||||
/** @public */
|
||||
export interface EntityTeamPullRequestsContentProps {
|
||||
|
|
@ -38,114 +27,14 @@ const EntityTeamPullRequestsContent = (
|
|||
props: EntityTeamPullRequestsContentProps,
|
||||
) => {
|
||||
const { pullRequestLimit } = props;
|
||||
const [infoCardFormat, setInfoCardFormat] = useState<PRCardFormating[]>([]);
|
||||
const {
|
||||
loading: loadingReposAndTeam,
|
||||
repositories,
|
||||
teamMembers,
|
||||
teamMembersOrganization,
|
||||
} = useUserRepositoriesAndTeam();
|
||||
const {
|
||||
loading: loadingPRs,
|
||||
pullRequests,
|
||||
refreshPullRequests,
|
||||
} = usePullRequestsByTeam(
|
||||
repositories,
|
||||
teamMembers,
|
||||
teamMembersOrganization,
|
||||
pullRequestLimit,
|
||||
const { entity: teamEntity } = useEntity();
|
||||
|
||||
return (
|
||||
<PullRequestsBoard
|
||||
entities={[teamEntity]}
|
||||
pullRequestLimit={pullRequestLimit}
|
||||
/>
|
||||
);
|
||||
|
||||
const header = (
|
||||
<InfoCardHeader onRefresh={refreshPullRequests}>
|
||||
<PullRequestBoardOptions
|
||||
onClickOption={newFormats => setInfoCardFormat(newFormats)}
|
||||
value={infoCardFormat}
|
||||
options={[
|
||||
{
|
||||
icon: <PeopleIcon />,
|
||||
value: 'team',
|
||||
ariaLabel: 'Show PRs from your team',
|
||||
},
|
||||
{
|
||||
icon: <DraftPrIcon />,
|
||||
value: 'draft',
|
||||
ariaLabel: 'Show draft PRs',
|
||||
},
|
||||
{
|
||||
icon: <UnarchiveIcon />,
|
||||
value: 'archivedRepo',
|
||||
ariaLabel: 'Show archived repos',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</InfoCardHeader>
|
||||
);
|
||||
|
||||
const getContent = () => {
|
||||
if (loadingReposAndTeam || loadingPRs) {
|
||||
return <Progress />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Grid container spacing={2}>
|
||||
{pullRequests.length ? (
|
||||
pullRequests.map(({ title: columnTitle, content }) => (
|
||||
<Wrapper key={columnTitle} fullscreen>
|
||||
<Typography variant="overline">{columnTitle}</Typography>
|
||||
{content.map(
|
||||
(
|
||||
{
|
||||
id,
|
||||
title,
|
||||
createdAt,
|
||||
lastEditedAt,
|
||||
author,
|
||||
url,
|
||||
latestReviews,
|
||||
commits,
|
||||
repository,
|
||||
isDraft,
|
||||
labels,
|
||||
},
|
||||
index,
|
||||
) =>
|
||||
shouldDisplayCard(
|
||||
repository,
|
||||
author,
|
||||
repositories,
|
||||
teamMembers,
|
||||
infoCardFormat,
|
||||
isDraft,
|
||||
) && (
|
||||
<PullRequestCard
|
||||
key={`pull-request-${id}-${index}`}
|
||||
title={title}
|
||||
createdAt={createdAt}
|
||||
updatedAt={lastEditedAt}
|
||||
author={author}
|
||||
url={url}
|
||||
reviews={latestReviews.nodes}
|
||||
status={commits.nodes}
|
||||
repositoryName={repository.name}
|
||||
repositoryIsArchived={repository.isArchived}
|
||||
isDraft={isDraft}
|
||||
labels={labels.nodes}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</Wrapper>
|
||||
))
|
||||
) : (
|
||||
<Typography variant="overline" data-testid="no-prs-msg">
|
||||
No pull requests found
|
||||
</Typography>
|
||||
)}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
return <InfoCard title={header}>{getContent()}</InfoCard>;
|
||||
};
|
||||
|
||||
export default EntityTeamPullRequestsContent;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,346 @@
|
|||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import PullRequestsBoard from './PullRequestsBoard';
|
||||
import { PullRequestsColumn, Status } from '../../utils/types';
|
||||
import { render } from '@testing-library/react';
|
||||
import { fireEvent } from '@testing-library/react';
|
||||
|
||||
const mockEntities = [
|
||||
{
|
||||
apiVersion: 'v1',
|
||||
kind: 'Group',
|
||||
metadata: {
|
||||
name: 'test-team',
|
||||
namespace: 'default',
|
||||
},
|
||||
spec: {
|
||||
type: 'team',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
jest.mock('../../hooks/useUserRepositoriesAndTeam', () => {
|
||||
return {
|
||||
useUserRepositoriesAndTeam: () => {
|
||||
return {
|
||||
loading: false,
|
||||
repositories: ['team-login/team-repo'],
|
||||
teamMembers: ['team-member'],
|
||||
teamMembersOrganization: 'test-org',
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../hooks/usePullRequestsByTeam', () => {
|
||||
const buildPullRequest = ({
|
||||
prTitle,
|
||||
authorLogin,
|
||||
repoName,
|
||||
isDraft,
|
||||
isArchived,
|
||||
status,
|
||||
}: {
|
||||
prTitle: string;
|
||||
authorLogin: string;
|
||||
repoName: string;
|
||||
isDraft: boolean;
|
||||
isArchived: boolean;
|
||||
status: Status;
|
||||
}) => {
|
||||
return {
|
||||
id: 'id',
|
||||
title: prTitle,
|
||||
url: 'url',
|
||||
lastEditedAt: 'last-edited-at',
|
||||
latestReviews: {
|
||||
nodes: [],
|
||||
},
|
||||
mergeable: true,
|
||||
state: 'state',
|
||||
reviewDecision: null,
|
||||
createdAt: 'created-at',
|
||||
repository: {
|
||||
name: repoName,
|
||||
owner: {
|
||||
login: 'team-login',
|
||||
},
|
||||
isArchived: isArchived,
|
||||
},
|
||||
labels: {
|
||||
nodes: [],
|
||||
},
|
||||
commits: {
|
||||
nodes: [status],
|
||||
},
|
||||
isDraft: isDraft,
|
||||
author: {
|
||||
login: authorLogin,
|
||||
avatarUrl: 'avatar-url',
|
||||
id: 'id',
|
||||
email: 'email',
|
||||
name: 'name',
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const pullRequests: PullRequestsColumn[] = [
|
||||
{
|
||||
title: 'column',
|
||||
content: [
|
||||
buildPullRequest({
|
||||
prTitle: 'non-team-non-draft-non-archive',
|
||||
authorLogin: 'non-team-member',
|
||||
repoName: 'team-repo',
|
||||
isDraft: false,
|
||||
isArchived: false,
|
||||
status: {
|
||||
commit: {
|
||||
statusCheckRollup: null,
|
||||
},
|
||||
},
|
||||
}),
|
||||
buildPullRequest({
|
||||
prTitle: 'non-team-non-draft-is-archive',
|
||||
authorLogin: 'non-team-member',
|
||||
repoName: 'team-repo',
|
||||
isDraft: false,
|
||||
isArchived: true,
|
||||
status: {
|
||||
commit: {
|
||||
statusCheckRollup: {
|
||||
state: 'FAILURE',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
buildPullRequest({
|
||||
prTitle: 'non-team-is-draft-non-archive',
|
||||
authorLogin: 'non-team-member',
|
||||
repoName: 'team-repo',
|
||||
isDraft: true,
|
||||
isArchived: false,
|
||||
status: {
|
||||
commit: {
|
||||
statusCheckRollup: {
|
||||
state: 'FAILURE',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
buildPullRequest({
|
||||
prTitle: 'non-team-is-draft-is-archive',
|
||||
authorLogin: 'non-team-member',
|
||||
repoName: 'team-repo',
|
||||
isDraft: true,
|
||||
isArchived: true,
|
||||
status: {
|
||||
commit: {
|
||||
statusCheckRollup: {
|
||||
state: 'SUCCESS',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
buildPullRequest({
|
||||
prTitle: 'is-team-non-draft-non-archive',
|
||||
authorLogin: 'team-member',
|
||||
repoName: 'non-team-repo',
|
||||
isDraft: false,
|
||||
isArchived: false,
|
||||
status: {
|
||||
commit: {
|
||||
statusCheckRollup: {
|
||||
state: 'FAILURE',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
buildPullRequest({
|
||||
prTitle: 'is-team-non-draft-is-archive',
|
||||
authorLogin: 'team-member',
|
||||
repoName: 'non-team-repo',
|
||||
isDraft: false,
|
||||
isArchived: true,
|
||||
status: {
|
||||
commit: {
|
||||
statusCheckRollup: {
|
||||
state: 'FAILURE',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
buildPullRequest({
|
||||
prTitle: 'is-team-is-draft-non-archive',
|
||||
authorLogin: 'team-member',
|
||||
repoName: 'non-team-repo',
|
||||
isDraft: true,
|
||||
isArchived: false,
|
||||
status: {
|
||||
commit: {
|
||||
statusCheckRollup: {
|
||||
state: 'FAILURE',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
buildPullRequest({
|
||||
prTitle: 'is-team-is-draft-is-archive',
|
||||
authorLogin: 'team-member',
|
||||
repoName: 'non-team-repo',
|
||||
isDraft: true,
|
||||
isArchived: true,
|
||||
status: {
|
||||
commit: {
|
||||
statusCheckRollup: {
|
||||
state: 'SUCCESS',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
return {
|
||||
usePullRequestsByTeam: () => {
|
||||
return {
|
||||
loading: false,
|
||||
pullRequests: pullRequests,
|
||||
refreshPullRequests: () => {},
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe('PullRequestsBoard', () => {
|
||||
describe('non-team PRs', () => {
|
||||
describe('non-draft PRs', () => {
|
||||
it('should show non-team PRs for un-archived repos when archived option is not checked', async () => {
|
||||
const { getByText, getAllByText, queryAllByTitle } = render(
|
||||
<PullRequestsBoard entities={mockEntities} />,
|
||||
);
|
||||
expect(getByText('non-team-non-draft-non-archive')).toBeInTheDocument();
|
||||
expect(getAllByText('team-repo')).toHaveLength(1);
|
||||
expect(queryAllByTitle('Repository is archived')).toHaveLength(0);
|
||||
expect(queryAllByTitle('Draft PR')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should show non-team PRs for archived repos when archived option is checked', async () => {
|
||||
const { getByText, getAllByText, getByTitle, queryAllByTitle } = render(
|
||||
<PullRequestsBoard entities={mockEntities} />,
|
||||
);
|
||||
const archiveToggle = getByTitle('Show archived repos');
|
||||
fireEvent.click(archiveToggle);
|
||||
expect(getByText('non-team-non-draft-is-archive')).toBeInTheDocument();
|
||||
expect(getAllByText('team-repo')).toHaveLength(1);
|
||||
expect(queryAllByTitle('Repository is archived')).toHaveLength(1);
|
||||
expect(queryAllByTitle('Draft PR')).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('draft PRs', () => {
|
||||
it('should show draft non-team PRs for un-archived repos when archived option is not checked', async () => {
|
||||
const { getByText, getAllByText, getByTitle, queryAllByTitle } = render(
|
||||
<PullRequestsBoard entities={mockEntities} />,
|
||||
);
|
||||
const draftToggle = getByTitle('Show draft PRs');
|
||||
fireEvent.click(draftToggle);
|
||||
expect(getByText('non-team-is-draft-non-archive')).toBeInTheDocument();
|
||||
expect(getAllByText('team-repo')).toHaveLength(1);
|
||||
expect(queryAllByTitle('Repository is archived')).toHaveLength(0);
|
||||
expect(queryAllByTitle('Draft PR')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should show draft non-team PRs for archived repos when archived option is checked', async () => {
|
||||
const { getByText, getAllByText, getByTitle, queryAllByTitle } = render(
|
||||
<PullRequestsBoard entities={mockEntities} />,
|
||||
);
|
||||
const draftToggle = getByTitle('Show draft PRs');
|
||||
fireEvent.click(draftToggle);
|
||||
const archiveToggle = getByTitle('Show archived repos');
|
||||
fireEvent.click(archiveToggle);
|
||||
expect(getByText('non-team-is-draft-is-archive')).toBeInTheDocument();
|
||||
expect(getAllByText('team-repo')).toHaveLength(1);
|
||||
expect(queryAllByTitle('Repository is archived')).toHaveLength(1);
|
||||
expect(queryAllByTitle('Draft PR')).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('team PRs', () => {
|
||||
describe('non-draft PRs', () => {
|
||||
it('should show team PRs for un-archived repos when archived option is not checked', async () => {
|
||||
const { getByText, getAllByText, getByTitle, queryAllByTitle } = render(
|
||||
<PullRequestsBoard entities={mockEntities} />,
|
||||
);
|
||||
const teamToggle = getByTitle('Show PRs from your team');
|
||||
fireEvent.click(teamToggle);
|
||||
expect(getByText('is-team-non-draft-non-archive')).toBeInTheDocument();
|
||||
expect(getAllByText('non-team-repo')).toHaveLength(1);
|
||||
expect(queryAllByTitle('Repository is archived')).toHaveLength(0);
|
||||
expect(queryAllByTitle('Draft PR')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should show team PRs for archived repos when archived option is checked', async () => {
|
||||
const { getByText, getAllByText, getByTitle, queryAllByTitle } = render(
|
||||
<PullRequestsBoard entities={mockEntities} />,
|
||||
);
|
||||
const teamToggle = getByTitle('Show PRs from your team');
|
||||
fireEvent.click(teamToggle);
|
||||
const archiveToggle = getByTitle('Show archived repos');
|
||||
fireEvent.click(archiveToggle);
|
||||
expect(getByText('is-team-non-draft-is-archive')).toBeInTheDocument();
|
||||
expect(getAllByText('non-team-repo')).toHaveLength(1);
|
||||
expect(queryAllByTitle('Repository is archived')).toHaveLength(1);
|
||||
expect(queryAllByTitle('Draft PR')).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('draft PRs', () => {
|
||||
it('should show draft team PRs for un-archived repos when archived option is not checked', async () => {
|
||||
const { getByText, getAllByText, getByTitle, queryAllByTitle } = render(
|
||||
<PullRequestsBoard entities={mockEntities} />,
|
||||
);
|
||||
const teamToggle = getByTitle('Show PRs from your team');
|
||||
fireEvent.click(teamToggle);
|
||||
const draftToggle = getByTitle('Show draft PRs');
|
||||
fireEvent.click(draftToggle);
|
||||
expect(getByText('is-team-is-draft-non-archive')).toBeInTheDocument();
|
||||
expect(getAllByText('non-team-repo')).toHaveLength(1);
|
||||
expect(queryAllByTitle('Repository is archived')).toHaveLength(0);
|
||||
expect(queryAllByTitle('Draft PR')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should show draft team PRs for archived repos when archived option is checked', async () => {
|
||||
const { getByText, getAllByText, getByTitle, queryAllByTitle } = render(
|
||||
<PullRequestsBoard entities={mockEntities} />,
|
||||
);
|
||||
const teamToggle = getByTitle('Show PRs from your team');
|
||||
fireEvent.click(teamToggle);
|
||||
const draftToggle = getByTitle('Show draft PRs');
|
||||
fireEvent.click(draftToggle);
|
||||
const archiveToggle = getByTitle('Show archived repos');
|
||||
fireEvent.click(archiveToggle);
|
||||
expect(getByText('is-team-is-draft-is-archive')).toBeInTheDocument();
|
||||
expect(getAllByText('non-team-repo')).toHaveLength(1);
|
||||
expect(queryAllByTitle('Repository is archived')).toHaveLength(1);
|
||||
expect(queryAllByTitle('Draft PR')).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,163 @@
|
|||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React, { useState } from 'react';
|
||||
import { Grid, Typography } from '@material-ui/core';
|
||||
import PeopleIcon from '@material-ui/icons/People';
|
||||
import { Progress, InfoCard } from '@backstage/core-components';
|
||||
|
||||
import { InfoCardHeader } from '../InfoCardHeader';
|
||||
import { PullRequestBoardOptions } from '../PullRequestBoardOptions';
|
||||
import { Wrapper } from '../Wrapper';
|
||||
import { PullRequestCard } from '../PullRequestCard';
|
||||
import { usePullRequestsByTeam } from '../../hooks/usePullRequestsByTeam';
|
||||
import { PRCardFormating } from '../../utils/types';
|
||||
import { shouldDisplayCard } from '../../utils/functions';
|
||||
import { DraftPrIcon } from '../icons/DraftPr';
|
||||
import UnarchiveIcon from '@material-ui/icons/Unarchive';
|
||||
import { useUserRepositoriesAndTeam } from '../../hooks/useUserRepositoriesAndTeam';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
|
||||
/** @public */
|
||||
export interface PullRequestsBoardProps {
|
||||
/**
|
||||
* List of entities to display pull requests for.
|
||||
* If not provided, the board will use the current user's repositories and team.
|
||||
*/
|
||||
entities: Entity[];
|
||||
|
||||
pullRequestLimit?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* A board component that displays pull requests for multiple entities.
|
||||
* It aggregates pull requests from the provided entities and allows filtering by team, draft status, and archived repositories.
|
||||
* @public
|
||||
* */
|
||||
const PullRequestsBoard = (props: PullRequestsBoardProps) => {
|
||||
const { entities, pullRequestLimit } = props;
|
||||
|
||||
const {
|
||||
loading: loadingReposAndTeams,
|
||||
repositories,
|
||||
teamMembers,
|
||||
teamMembersOrganization,
|
||||
} = useUserRepositoriesAndTeam(entities);
|
||||
const [infoCardFormat, setInfoCardFormat] = useState<PRCardFormating[]>([]);
|
||||
|
||||
const {
|
||||
loading: loadingPRs,
|
||||
pullRequests,
|
||||
refreshPullRequests,
|
||||
} = usePullRequestsByTeam(
|
||||
repositories,
|
||||
teamMembers,
|
||||
teamMembersOrganization,
|
||||
pullRequestLimit,
|
||||
);
|
||||
|
||||
const header = (
|
||||
<InfoCardHeader onRefresh={refreshPullRequests}>
|
||||
<PullRequestBoardOptions
|
||||
onClickOption={newFormats => setInfoCardFormat(newFormats)}
|
||||
value={infoCardFormat}
|
||||
options={[
|
||||
{
|
||||
icon: <PeopleIcon />,
|
||||
value: 'team',
|
||||
ariaLabel: 'Show PRs from your team',
|
||||
},
|
||||
{
|
||||
icon: <DraftPrIcon />,
|
||||
value: 'draft',
|
||||
ariaLabel: 'Show draft PRs',
|
||||
},
|
||||
{
|
||||
icon: <UnarchiveIcon />,
|
||||
value: 'archivedRepo',
|
||||
ariaLabel: 'Show archived repos',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</InfoCardHeader>
|
||||
);
|
||||
|
||||
const getContent = () => {
|
||||
if (loadingReposAndTeams || loadingPRs) {
|
||||
return <Progress />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Grid container spacing={2}>
|
||||
{pullRequests.length ? (
|
||||
pullRequests.map(({ title: columnTitle, content }) => (
|
||||
<Wrapper key={columnTitle} fullscreen>
|
||||
<Typography variant="overline">{columnTitle}</Typography>
|
||||
{content.map(
|
||||
(
|
||||
{
|
||||
id,
|
||||
title,
|
||||
createdAt,
|
||||
lastEditedAt,
|
||||
author,
|
||||
url,
|
||||
latestReviews,
|
||||
commits,
|
||||
repository,
|
||||
isDraft,
|
||||
labels,
|
||||
},
|
||||
index,
|
||||
) =>
|
||||
shouldDisplayCard(
|
||||
repository,
|
||||
author,
|
||||
repositories,
|
||||
teamMembers,
|
||||
infoCardFormat,
|
||||
isDraft,
|
||||
) && (
|
||||
<PullRequestCard
|
||||
key={`pull-request-${id}-${index}`}
|
||||
title={title}
|
||||
createdAt={createdAt}
|
||||
updatedAt={lastEditedAt}
|
||||
author={author}
|
||||
url={url}
|
||||
reviews={latestReviews.nodes}
|
||||
status={commits.nodes}
|
||||
repositoryName={repository.name}
|
||||
repositoryIsArchived={repository.isArchived}
|
||||
isDraft={isDraft}
|
||||
labels={labels.nodes}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</Wrapper>
|
||||
))
|
||||
) : (
|
||||
<Typography variant="overline" data-testid="no-prs-msg">
|
||||
No pull requests found
|
||||
</Typography>
|
||||
)}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
return <InfoCard title={header}>{getContent()}</InfoCard>;
|
||||
};
|
||||
|
||||
export default PullRequestsBoard;
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export { default } from './PullRequestsBoard';
|
||||
export type { PullRequestsBoardProps } from './PullRequestsBoard';
|
||||
|
|
@ -0,0 +1,210 @@
|
|||
/*
|
||||
* Copyright 2025 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 { renderHook, waitFor } from '@testing-library/react';
|
||||
import { TestApiProvider } from '@backstage/test-utils';
|
||||
import { catalogApiRef } from '@backstage/plugin-catalog-react';
|
||||
import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { useUserRepositoriesAndTeam } from './useUserRepositoriesAndTeam';
|
||||
import React from 'react';
|
||||
|
||||
const mockTeamEntity: Entity = {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Group',
|
||||
metadata: {
|
||||
name: 'team-one',
|
||||
namespace: 'default',
|
||||
annotations: {
|
||||
'github.com/team-slug': 'test-org/team-one',
|
||||
},
|
||||
},
|
||||
spec: {
|
||||
type: 'team',
|
||||
},
|
||||
};
|
||||
|
||||
const mockTeamTwoEntity: Entity = {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Group',
|
||||
metadata: {
|
||||
name: 'team-two',
|
||||
namespace: 'default',
|
||||
annotations: {
|
||||
'github.com/team-slug': 'test-org/team-two',
|
||||
},
|
||||
},
|
||||
spec: {
|
||||
type: 'team',
|
||||
},
|
||||
};
|
||||
|
||||
const mockComponentEntity: Entity = {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'repo-one',
|
||||
namespace: 'default',
|
||||
annotations: {
|
||||
'github.com/project-slug': 'test-org/repo-one',
|
||||
},
|
||||
},
|
||||
spec: {
|
||||
type: 'service',
|
||||
lifecycle: 'production',
|
||||
owner: 'group:default/team-one',
|
||||
},
|
||||
relations: [
|
||||
{
|
||||
type: 'ownedBy',
|
||||
targetRef: 'group:default/team-one',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const mockUserEntity: Entity = {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'User',
|
||||
metadata: {
|
||||
name: 'user-one',
|
||||
namespace: 'default',
|
||||
annotations: {
|
||||
'github.com/user-login': 'user-one-github',
|
||||
},
|
||||
},
|
||||
spec: {
|
||||
profile: {
|
||||
displayName: 'User One',
|
||||
},
|
||||
},
|
||||
relations: [
|
||||
{
|
||||
type: 'memberOf',
|
||||
targetRef: 'group:default/team-one',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const mockRepoTwoEntity: Entity = {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'repo-two',
|
||||
namespace: 'default',
|
||||
annotations: {
|
||||
'github.com/project-slug': 'test-org/repo-two',
|
||||
},
|
||||
},
|
||||
spec: {
|
||||
type: 'service',
|
||||
lifecycle: 'production',
|
||||
owner: 'group:default/team-two',
|
||||
},
|
||||
relations: [
|
||||
{
|
||||
type: 'ownedBy',
|
||||
targetRef: 'group:default/team-two',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const mockUserTwoEntity: Entity = {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'User',
|
||||
metadata: {
|
||||
name: 'user-two',
|
||||
namespace: 'default',
|
||||
annotations: {
|
||||
'github.com/user-login': 'user-two-github',
|
||||
},
|
||||
},
|
||||
spec: {
|
||||
profile: {
|
||||
displayName: 'User Two',
|
||||
},
|
||||
},
|
||||
relations: [
|
||||
{
|
||||
type: 'memberOf',
|
||||
targetRef: 'group:default/team-two',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const catalogApi = catalogApiMock({
|
||||
entities: [
|
||||
mockTeamEntity,
|
||||
mockTeamTwoEntity,
|
||||
mockComponentEntity,
|
||||
mockRepoTwoEntity,
|
||||
mockUserEntity,
|
||||
mockUserTwoEntity,
|
||||
],
|
||||
});
|
||||
|
||||
describe('useUserRepositoriesAndTeam', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<TestApiProvider apis={[[catalogApiRef, catalogApi]]}>
|
||||
{children}
|
||||
</TestApiProvider>
|
||||
);
|
||||
|
||||
it('should return repositories and team members for a team entity', async () => {
|
||||
const { result } = renderHook(
|
||||
() => useUserRepositoriesAndTeam(mockTeamEntity),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
// Initially should be loading
|
||||
expect(result.current.loading).toBe(true);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
|
||||
expect(result.current.repositories).toEqual(['test-org/repo-one']);
|
||||
expect(result.current.teamMembers).toEqual(['user-one-github']);
|
||||
expect(result.current.teamMembersOrganization).toBe('test-org');
|
||||
});
|
||||
|
||||
it('should return repositories and team members for multiple team entities', async () => {
|
||||
const { result } = renderHook(
|
||||
() => useUserRepositoriesAndTeam([mockTeamEntity, mockTeamTwoEntity]),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
// Initially should be loading
|
||||
expect(result.current.loading).toBe(true);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
|
||||
// Should aggregate repositories and team members from both teams
|
||||
expect(result.current.repositories).toEqual([
|
||||
'test-org/repo-one',
|
||||
'test-org/repo-two',
|
||||
]);
|
||||
expect(result.current.teamMembers).toEqual([
|
||||
'user-one-github',
|
||||
'user-two-github',
|
||||
]);
|
||||
expect(result.current.teamMembersOrganization).toBe('test-org');
|
||||
});
|
||||
});
|
||||
|
|
@ -14,9 +14,9 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { stringifyEntityRef } from '@backstage/catalog-model';
|
||||
import { Entity, stringifyEntityRef } from '@backstage/catalog-model';
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
import { catalogApiRef, useEntity } from '@backstage/plugin-catalog-react';
|
||||
import { catalogApiRef } from '@backstage/plugin-catalog-react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import {
|
||||
getProjectNameFromEntity,
|
||||
|
|
@ -24,8 +24,11 @@ import {
|
|||
getUserNameFromEntity,
|
||||
} from '../utils/functions';
|
||||
|
||||
export function useUserRepositoriesAndTeam() {
|
||||
const { entity: teamEntity } = useEntity();
|
||||
export function useUserRepositoriesAndTeam(teamEntities: Entity | Entity[]) {
|
||||
const ownerEntities = Array.isArray(teamEntities)
|
||||
? teamEntities
|
||||
: [teamEntities];
|
||||
|
||||
const catalogApi = useApi(catalogApiRef);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [teamData, setTeamData] = useState<{
|
||||
|
|
@ -36,14 +39,18 @@ export function useUserRepositoriesAndTeam() {
|
|||
teamMembers: [],
|
||||
});
|
||||
|
||||
const entitiesRefs = ownerEntities.map(e => stringifyEntityRef(e));
|
||||
|
||||
const getTeamData = useCallback(async () => {
|
||||
setLoading(true);
|
||||
|
||||
// get team repositories and members
|
||||
const entitiesList = await catalogApi.getEntities({
|
||||
filter: [
|
||||
{ 'relations.ownedBy': stringifyEntityRef(teamEntity) },
|
||||
{ 'relations.memberOf': stringifyEntityRef(teamEntity) },
|
||||
{ 'relations.ownedBy': entitiesRefs },
|
||||
{
|
||||
'relations.memberOf': entitiesRefs,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
|
|
@ -66,7 +73,8 @@ export function useUserRepositoriesAndTeam() {
|
|||
teamMembers: [...new Set(teamMembersNames)],
|
||||
});
|
||||
setLoading(false);
|
||||
}, [catalogApi, teamEntity]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [catalogApi, entitiesRefs.join(',')]);
|
||||
|
||||
useEffect(() => {
|
||||
getTeamData();
|
||||
|
|
@ -76,6 +84,6 @@ export function useUserRepositoriesAndTeam() {
|
|||
loading,
|
||||
repositories: teamData.repositories,
|
||||
teamMembers: teamData.teamMembers,
|
||||
teamMembersOrganization: getGithubOrganizationFromEntity(teamEntity),
|
||||
teamMembersOrganization: getGithubOrganizationFromEntity(ownerEntities[0]),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,5 +17,7 @@ export {
|
|||
EntityTeamPullRequestsCard,
|
||||
EntityTeamPullRequestsContent,
|
||||
} from './plugin';
|
||||
export { default as PullRequestsBoard } from './components/PullRequestsBoard';
|
||||
export type { EntityTeamPullRequestsCardProps } from './components/EntityTeamPullRequestsCard';
|
||||
export type { EntityTeamPullRequestsContentProps } from './components/EntityTeamPullRequestsContent';
|
||||
export type { PullRequestsBoardProps } from './components/PullRequestsBoard';
|
||||
|
|
|
|||
|
|
@ -52,3 +52,6 @@ export const EntityTeamPullRequestsContent =
|
|||
mountPoint: rootRouteRef,
|
||||
}),
|
||||
);
|
||||
|
||||
/** @public */
|
||||
export default githubPullRequestsBoardPlugin;
|
||||
|
|
|
|||
|
|
@ -1738,6 +1738,7 @@ __metadata:
|
|||
"@backstage/core-compat-api": "npm:^0.4.4"
|
||||
"@backstage/core-components": "npm:^0.17.4"
|
||||
"@backstage/core-plugin-api": "npm:^1.10.9"
|
||||
"@backstage/dev-utils": "npm:^1.1.12"
|
||||
"@backstage/frontend-plugin-api": "npm:^0.10.4"
|
||||
"@backstage/frontend-test-utils": "npm:^0.3.4"
|
||||
"@backstage/integration": "npm:^1.17.1"
|
||||
|
|
@ -1764,6 +1765,29 @@ __metadata:
|
|||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@backstage/app-defaults@npm:^1.6.4":
|
||||
version: 1.6.4
|
||||
resolution: "@backstage/app-defaults@npm:1.6.4"
|
||||
dependencies:
|
||||
"@backstage/core-app-api": "npm:^1.18.0"
|
||||
"@backstage/core-components": "npm:^0.17.4"
|
||||
"@backstage/core-plugin-api": "npm:^1.10.9"
|
||||
"@backstage/plugin-permission-react": "npm:^0.4.36"
|
||||
"@backstage/theme": "npm:^0.6.7"
|
||||
"@material-ui/core": "npm:^4.12.2"
|
||||
"@material-ui/icons": "npm:^4.9.1"
|
||||
peerDependencies:
|
||||
"@types/react": ^17.0.0 || ^18.0.0
|
||||
react: ^17.0.0 || ^18.0.0
|
||||
react-dom: ^17.0.0 || ^18.0.0
|
||||
react-router-dom: ^6.3.0
|
||||
peerDependenciesMeta:
|
||||
"@types/react":
|
||||
optional: true
|
||||
checksum: 10/fb88e41cde626ac91b277ade2f2b2e881cffc61d76497569b483f84231947c2afce7259d2f79e43fadfb02c40254c4f3426b3890a29b26ab522a9c7e2d0135a1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@backstage/backend-plugin-api@npm:^1.4.1":
|
||||
version: 1.4.1
|
||||
resolution: "@backstage/backend-plugin-api@npm:1.4.1"
|
||||
|
|
@ -2126,6 +2150,33 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@backstage/dev-utils@npm:^1.1.12":
|
||||
version: 1.1.12
|
||||
resolution: "@backstage/dev-utils@npm:1.1.12"
|
||||
dependencies:
|
||||
"@backstage/app-defaults": "npm:^1.6.4"
|
||||
"@backstage/catalog-model": "npm:^1.7.5"
|
||||
"@backstage/core-app-api": "npm:^1.18.0"
|
||||
"@backstage/core-components": "npm:^0.17.4"
|
||||
"@backstage/core-plugin-api": "npm:^1.10.9"
|
||||
"@backstage/integration-react": "npm:^1.2.9"
|
||||
"@backstage/plugin-catalog-react": "npm:^1.19.1"
|
||||
"@backstage/theme": "npm:^0.6.7"
|
||||
"@material-ui/core": "npm:^4.12.2"
|
||||
"@material-ui/icons": "npm:^4.9.1"
|
||||
react-use: "npm:^17.2.4"
|
||||
peerDependencies:
|
||||
"@types/react": ^17.0.0 || ^18.0.0
|
||||
react: ^17.0.0 || ^18.0.0
|
||||
react-dom: ^17.0.0 || ^18.0.0
|
||||
react-router-dom: ^6.3.0
|
||||
peerDependenciesMeta:
|
||||
"@types/react":
|
||||
optional: true
|
||||
checksum: 10/1a00f6a7b7ccdcbb3fd1c9c144687fd29e04698d4f846b63d4294c8e5136c8ed640f5fb3da88109cbcce9eb73355b07d0a59b15a8486fe55c1e61634fb60fd3f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@backstage/e2e-test-utils@npm:^0.1.1":
|
||||
version: 0.1.1
|
||||
resolution: "@backstage/e2e-test-utils@npm:0.1.1"
|
||||
|
|
|
|||
Loading…
Reference in New Issue