Merge pull request #13837 from Cylix/github-team-pull-requests
plugin-github-pull-requests-board: Add a new "Team" Filter Options to the Github Pull Requests Dashboard.
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
---
|
||||
'@backstage/plugin-github-pull-requests-board': patch
|
||||
---
|
||||
|
||||
Add a new "Team" Filter Options to the Github Pull Requests Dashboard.
|
||||
|
||||
When toggling this option on, the dashboard will displays all of the PRs opened
|
||||
by the members of that team on any repositories of the organization.
|
||||
@@ -45,6 +45,7 @@
|
||||
"@material-ui/lab": "4.0.0-alpha.57",
|
||||
"@octokit/rest": "^19.0.3",
|
||||
"luxon": "^3.0.0",
|
||||
"p-limit": "^4.0.0",
|
||||
"react-use": "^17.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -33,6 +33,9 @@ export const useGetPullRequestDetails = () => {
|
||||
id
|
||||
repository {
|
||||
name
|
||||
owner {
|
||||
login
|
||||
}
|
||||
}
|
||||
title
|
||||
url
|
||||
|
||||
@@ -33,7 +33,7 @@ export const useGetPullRequestsFromRepository = () => {
|
||||
const limit = pullRequestLimit ?? PULL_REQUEST_LIMIT;
|
||||
const [organisation, repositoryName] = repo.split('/');
|
||||
|
||||
return await getPullRequestEdges(
|
||||
return await getPullRequestNodes(
|
||||
graphql,
|
||||
repositoryName,
|
||||
organisation,
|
||||
@@ -45,7 +45,7 @@ export const useGetPullRequestsFromRepository = () => {
|
||||
return fn.current;
|
||||
};
|
||||
|
||||
async function getPullRequestEdges(
|
||||
async function getPullRequestNodes(
|
||||
graphql: (
|
||||
path: string,
|
||||
options?: any,
|
||||
@@ -54,7 +54,7 @@ async function getPullRequestEdges(
|
||||
organisation: string,
|
||||
pullRequestLimit: number,
|
||||
): Promise<PullRequestsNumber[]> {
|
||||
const pullRequestEdges: PullRequestsNumber[] = [];
|
||||
const pullRequestNodes: PullRequestsNumber[] = [];
|
||||
let result: GraphQlPullRequests<PullRequestsNumber[]> | undefined = undefined;
|
||||
|
||||
do {
|
||||
@@ -68,10 +68,8 @@ async function getPullRequestEdges(
|
||||
) {
|
||||
repository(name: $name, owner: $owner) {
|
||||
pullRequests(states: OPEN, first: $first, after: $endCursor) {
|
||||
edges {
|
||||
node {
|
||||
number
|
||||
}
|
||||
nodes {
|
||||
number
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
@@ -94,10 +92,10 @@ async function getPullRequestEdges(
|
||||
},
|
||||
);
|
||||
|
||||
pullRequestEdges.push(...result.repository.pullRequests.edges);
|
||||
pullRequestNodes.push(...result.repository.pullRequests.nodes);
|
||||
|
||||
if (pullRequestEdges.length >= pullRequestLimit) return pullRequestEdges;
|
||||
if (pullRequestNodes.length >= pullRequestLimit) return pullRequestNodes;
|
||||
} while (result.repository.pullRequests.pageInfo.hasNextPage);
|
||||
|
||||
return pullRequestEdges;
|
||||
return pullRequestNodes;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* 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 from 'react';
|
||||
|
||||
import {
|
||||
GraphQlUserPullRequests,
|
||||
PullRequestsNumberAndOwner,
|
||||
} from '../utils/types';
|
||||
import { useOctokitGraphQl } from './useOctokitGraphQl';
|
||||
|
||||
const PULL_REQUEST_LIMIT = 10;
|
||||
const GITHUB_GRAPHQL_MAX_ITEMS = 100;
|
||||
|
||||
export const useGetPullRequestsFromUser = () => {
|
||||
const graphql =
|
||||
useOctokitGraphQl<GraphQlUserPullRequests<PullRequestsNumberAndOwner[]>>();
|
||||
|
||||
const fn = React.useRef(
|
||||
async (
|
||||
userLogin: string,
|
||||
organization?: string,
|
||||
pullRequestLimit?: number,
|
||||
): Promise<PullRequestsNumberAndOwner[]> => {
|
||||
const limit = pullRequestLimit ?? PULL_REQUEST_LIMIT;
|
||||
|
||||
return await getPullRequestNodes(graphql, userLogin, limit, organization);
|
||||
},
|
||||
);
|
||||
|
||||
return fn.current;
|
||||
};
|
||||
|
||||
async function getPullRequestNodes(
|
||||
graphql: (
|
||||
path: string,
|
||||
options?: any,
|
||||
) => Promise<GraphQlUserPullRequests<PullRequestsNumberAndOwner[]>>,
|
||||
userLogin: string,
|
||||
pullRequestLimit: number,
|
||||
organization?: string,
|
||||
): Promise<PullRequestsNumberAndOwner[]> {
|
||||
const pullRequestNodes: PullRequestsNumberAndOwner[] = [];
|
||||
let result:
|
||||
| GraphQlUserPullRequests<PullRequestsNumberAndOwner[]>
|
||||
| undefined = undefined;
|
||||
|
||||
do {
|
||||
result = await graphql(
|
||||
`
|
||||
query ($login: String!, $first: Int, $endCursor: String) {
|
||||
user(login: $login) {
|
||||
pullRequests(states: OPEN, first: $first, after: $endCursor) {
|
||||
nodes {
|
||||
number
|
||||
repository {
|
||||
name
|
||||
owner {
|
||||
login
|
||||
}
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
endCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
login: userLogin,
|
||||
first:
|
||||
pullRequestLimit > GITHUB_GRAPHQL_MAX_ITEMS
|
||||
? GITHUB_GRAPHQL_MAX_ITEMS
|
||||
: pullRequestLimit,
|
||||
endCursor: result
|
||||
? result.user.pullRequests.pageInfo.endCursor
|
||||
: undefined,
|
||||
},
|
||||
);
|
||||
|
||||
pullRequestNodes.push(
|
||||
...result.user.pullRequests.nodes.filter(
|
||||
edge =>
|
||||
!organization ||
|
||||
edge?.repository?.owner?.login?.toLocaleLowerCase('en-US') ===
|
||||
organization.toLocaleLowerCase('en-US'),
|
||||
),
|
||||
);
|
||||
|
||||
if (pullRequestNodes.length >= pullRequestLimit) return pullRequestNodes;
|
||||
} while (result.user.pullRequests.pageInfo.hasNextPage);
|
||||
|
||||
return pullRequestNodes;
|
||||
}
|
||||
+31
-6
@@ -16,6 +16,7 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Grid, Typography } from '@material-ui/core';
|
||||
import FullscreenIcon from '@material-ui/icons/Fullscreen';
|
||||
import PeopleIcon from '@material-ui/icons/People';
|
||||
|
||||
import { Progress, InfoCard } from '@backstage/core-components';
|
||||
|
||||
@@ -25,8 +26,9 @@ 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 { useUserRepositories } from '../../hooks/useUserRepositories';
|
||||
import { useUserRepositoriesAndTeam } from '../../hooks/useUserRepositoriesAndTeam';
|
||||
|
||||
/** @public */
|
||||
export interface EntityTeamPullRequestsCardProps {
|
||||
@@ -36,9 +38,20 @@ export interface EntityTeamPullRequestsCardProps {
|
||||
const EntityTeamPullRequestsCard = (props: EntityTeamPullRequestsCardProps) => {
|
||||
const { pullRequestLimit } = props;
|
||||
const [infoCardFormat, setInfoCardFormat] = useState<PRCardFormating[]>([]);
|
||||
const { repositories } = useUserRepositories();
|
||||
const { loading, pullRequests, refreshPullRequests } = usePullRequestsByTeam(
|
||||
const {
|
||||
loading: loadingReposAndTeam,
|
||||
repositories,
|
||||
teamMembers,
|
||||
teamMembersOrganization,
|
||||
} = useUserRepositoriesAndTeam();
|
||||
const {
|
||||
loading: loadingPRs,
|
||||
pullRequests,
|
||||
refreshPullRequests,
|
||||
} = usePullRequestsByTeam(
|
||||
repositories,
|
||||
teamMembers,
|
||||
teamMembersOrganization,
|
||||
pullRequestLimit,
|
||||
);
|
||||
|
||||
@@ -48,6 +61,11 @@ const EntityTeamPullRequestsCard = (props: EntityTeamPullRequestsCardProps) => {
|
||||
onClickOption={newFormats => setInfoCardFormat(newFormats)}
|
||||
value={infoCardFormat}
|
||||
options={[
|
||||
{
|
||||
icon: <PeopleIcon />,
|
||||
value: 'team',
|
||||
ariaLabel: 'Show PRs from your team',
|
||||
},
|
||||
{
|
||||
icon: <DraftPrIcon />,
|
||||
value: 'draft',
|
||||
@@ -56,7 +74,7 @@ const EntityTeamPullRequestsCard = (props: EntityTeamPullRequestsCardProps) => {
|
||||
{
|
||||
icon: <FullscreenIcon />,
|
||||
value: 'fullscreen',
|
||||
ariaLabel: 'Info card is set to fullscreen',
|
||||
ariaLabel: 'Set card to fullscreen',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
@@ -64,7 +82,7 @@ const EntityTeamPullRequestsCard = (props: EntityTeamPullRequestsCardProps) => {
|
||||
);
|
||||
|
||||
const getContent = () => {
|
||||
if (loading) {
|
||||
if (loadingReposAndTeam || loadingPRs) {
|
||||
return <Progress />;
|
||||
}
|
||||
|
||||
@@ -92,7 +110,14 @@ const EntityTeamPullRequestsCard = (props: EntityTeamPullRequestsCardProps) => {
|
||||
},
|
||||
index,
|
||||
) =>
|
||||
infoCardFormat.includes('draft') === isDraft && (
|
||||
shouldDisplayCard(
|
||||
repository,
|
||||
author,
|
||||
repositories,
|
||||
teamMembers,
|
||||
infoCardFormat,
|
||||
isDraft,
|
||||
) && (
|
||||
<PullRequestCard
|
||||
key={`pull-request-${id}-${index}`}
|
||||
title={title}
|
||||
|
||||
+30
-5
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
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';
|
||||
@@ -23,8 +24,9 @@ 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 { useUserRepositories } from '../../hooks/useUserRepositories';
|
||||
import { useUserRepositoriesAndTeam } from '../../hooks/useUserRepositoriesAndTeam';
|
||||
|
||||
/** @public */
|
||||
export interface EntityTeamPullRequestsContentProps {
|
||||
@@ -36,9 +38,20 @@ const EntityTeamPullRequestsContent = (
|
||||
) => {
|
||||
const { pullRequestLimit } = props;
|
||||
const [infoCardFormat, setInfoCardFormat] = useState<PRCardFormating[]>([]);
|
||||
const { repositories } = useUserRepositories();
|
||||
const { loading, pullRequests, refreshPullRequests } = usePullRequestsByTeam(
|
||||
const {
|
||||
loading: loadingReposAndTeam,
|
||||
repositories,
|
||||
teamMembers,
|
||||
teamMembersOrganization,
|
||||
} = useUserRepositoriesAndTeam();
|
||||
const {
|
||||
loading: loadingPRs,
|
||||
pullRequests,
|
||||
refreshPullRequests,
|
||||
} = usePullRequestsByTeam(
|
||||
repositories,
|
||||
teamMembers,
|
||||
teamMembersOrganization,
|
||||
pullRequestLimit,
|
||||
);
|
||||
|
||||
@@ -48,6 +61,11 @@ const EntityTeamPullRequestsContent = (
|
||||
onClickOption={newFormats => setInfoCardFormat(newFormats)}
|
||||
value={infoCardFormat}
|
||||
options={[
|
||||
{
|
||||
icon: <PeopleIcon />,
|
||||
value: 'team',
|
||||
ariaLabel: 'Show PRs from your team',
|
||||
},
|
||||
{
|
||||
icon: <DraftPrIcon />,
|
||||
value: 'draft',
|
||||
@@ -59,7 +77,7 @@ const EntityTeamPullRequestsContent = (
|
||||
);
|
||||
|
||||
const getContent = () => {
|
||||
if (loading) {
|
||||
if (loadingReposAndTeam || loadingPRs) {
|
||||
return <Progress />;
|
||||
}
|
||||
|
||||
@@ -84,7 +102,14 @@ const EntityTeamPullRequestsContent = (
|
||||
},
|
||||
index,
|
||||
) =>
|
||||
infoCardFormat.includes('draft') === isDraft && (
|
||||
shouldDisplayCard(
|
||||
repository,
|
||||
author,
|
||||
repositories,
|
||||
teamMembers,
|
||||
infoCardFormat,
|
||||
isDraft,
|
||||
) && (
|
||||
<PullRequestCard
|
||||
key={`pull-request-${id}-${index}`}
|
||||
title={title}
|
||||
|
||||
@@ -24,8 +24,16 @@ const DraftPr: FunctionComponent = () => (
|
||||
width="16"
|
||||
data-view-component="true"
|
||||
>
|
||||
<path d="M2.5 3.25a.75.75 0 111.5 0 .75.75 0 01-1.5 0zM3.25 1a2.25 2.25 0 00-.75 4.372v5.256a2.251 2.251 0 101.5 0V5.372A2.25 2.25 0 003.25 1zm0 11a.75.75 0 100 1.5.75.75 0 000-1.5zm9.5 3a2.25 2.25 0 100-4.5 2.25 2.25 0 000 4.5zm0-3a.75.75 0 100 1.5.75.75 0 000-1.5z" />
|
||||
<path d="M14 7.5a1.25 1.25 0 11-2.5 0 1.25 1.25 0 012.5 0zm0-4.25a1.25 1.25 0 11-2.5 0 1.25 1.25 0 012.5 0z" />
|
||||
<g color="rgba(0, 0, 0, 0.38)">
|
||||
<path
|
||||
fill="currentcolor"
|
||||
d="M2.5 3.25a.75.75 0 111.5 0 .75.75 0 01-1.5 0zM3.25 1a2.25 2.25 0 00-.75 4.372v5.256a2.251 2.251 0 101.5 0V5.372A2.25 2.25 0 003.25 1zm0 11a.75.75 0 100 1.5.75.75 0 000-1.5zm9.5 3a2.25 2.25 0 100-4.5 2.25 2.25 0 000 4.5zm0-3a.75.75 0 100 1.5.75.75 0 000-1.5z"
|
||||
/>
|
||||
<path
|
||||
fill="currentcolor"
|
||||
d="M14 7.5a1.25 1.25 0 11-2.5 0 1.25 1.25 0 012.5 0zm0-4.25a1.25 1.25 0 11-2.5 0 1.25 1.25 0 012.5 0z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
|
||||
|
||||
@@ -13,48 +13,93 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import pLimit from 'p-limit';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { formatPRsByReviewDecision } from '../utils/functions';
|
||||
import { PullRequests, PullRequestsColumn } from '../utils/types';
|
||||
import { useGetPullRequestsFromRepository } from '../api/useGetPullRequestsFromRepository';
|
||||
import { useGetPullRequestsFromUser } from '../api/useGetPullRequestsFromUser';
|
||||
import { useGetPullRequestDetails } from '../api/useGetPullRequestDetails';
|
||||
|
||||
export function usePullRequestsByTeam(
|
||||
repositories: string[],
|
||||
members: string[],
|
||||
organization?: string,
|
||||
pullRequestLimit?: number,
|
||||
) {
|
||||
const [pullRequests, setPullRequests] = useState<PullRequestsColumn[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const getPullRequests = useGetPullRequestsFromRepository();
|
||||
const getPullRequestsFromRepository = useGetPullRequestsFromRepository();
|
||||
const getPullRequestsFromUser = useGetPullRequestsFromUser();
|
||||
const getPullRequestDetails = useGetPullRequestDetails();
|
||||
|
||||
const getPRsPerRepository = useCallback(
|
||||
async (repository: string): Promise<PullRequests> => {
|
||||
const pullRequestsNumbers = await getPullRequests(
|
||||
const concurrencyLimit = pLimit(5);
|
||||
const pullRequestsNumbers = await getPullRequestsFromRepository(
|
||||
repository,
|
||||
pullRequestLimit,
|
||||
);
|
||||
|
||||
const pullRequestsWithDetails = await Promise.all(
|
||||
pullRequestsNumbers.map(({ node }) =>
|
||||
getPullRequestDetails(repository, node.number),
|
||||
pullRequestsNumbers.map(node =>
|
||||
concurrencyLimit(() =>
|
||||
getPullRequestDetails(repository, node.number),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return pullRequestsWithDetails;
|
||||
},
|
||||
[getPullRequests, getPullRequestDetails, pullRequestLimit],
|
||||
[getPullRequestsFromRepository, getPullRequestDetails, pullRequestLimit],
|
||||
);
|
||||
|
||||
const getPRsPerTeamMember = useCallback(
|
||||
async (
|
||||
teamMember: string,
|
||||
teamOrganization?: string,
|
||||
): Promise<PullRequests> => {
|
||||
const concurrencyLimit = pLimit(3);
|
||||
const pullRequestsNumbers = await getPullRequestsFromUser(
|
||||
teamMember,
|
||||
teamOrganization,
|
||||
pullRequestLimit,
|
||||
);
|
||||
|
||||
const pullRequestsWithDetails = await Promise.all(
|
||||
pullRequestsNumbers.map(node =>
|
||||
concurrencyLimit(() =>
|
||||
getPullRequestDetails(
|
||||
`${node.repository.owner.login}/${node.repository.name}`,
|
||||
node.number,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return pullRequestsWithDetails;
|
||||
},
|
||||
[getPullRequestsFromUser, getPullRequestDetails, pullRequestLimit],
|
||||
);
|
||||
|
||||
const getPRsFromTeam = useCallback(
|
||||
async (teamRepositories: string[]): Promise<PullRequests> => {
|
||||
async (
|
||||
teamRepositories: string[],
|
||||
teamMembers: string[],
|
||||
teamOrganization?: string,
|
||||
): Promise<PullRequests> => {
|
||||
const teamRepositoriesPromises = teamRepositories.map(repository =>
|
||||
getPRsPerRepository(repository),
|
||||
);
|
||||
|
||||
const teamPullRequests = await Promise.allSettled(
|
||||
teamRepositoriesPromises,
|
||||
).then(promises =>
|
||||
const teamMembersPromises = teamMembers.map(teamMember =>
|
||||
getPRsPerTeamMember(teamMember, teamOrganization),
|
||||
);
|
||||
|
||||
const teamPullRequests = await Promise.allSettled([
|
||||
...teamRepositoriesPromises,
|
||||
...teamMembersPromises,
|
||||
]).then(promises =>
|
||||
promises.reduce((acc, curr) => {
|
||||
if (curr.status === 'fulfilled') {
|
||||
return [...acc, ...curr.value];
|
||||
@@ -63,18 +108,26 @@ export function usePullRequestsByTeam(
|
||||
}, [] as PullRequests),
|
||||
);
|
||||
|
||||
return teamPullRequests;
|
||||
const uniqueTeamPullRequests = teamPullRequests.filter(
|
||||
(lhs, i) => teamPullRequests.findIndex(rhs => lhs.id === rhs.id) === i,
|
||||
);
|
||||
|
||||
return uniqueTeamPullRequests;
|
||||
},
|
||||
[getPRsPerRepository],
|
||||
[getPRsPerRepository, getPRsPerTeamMember],
|
||||
);
|
||||
|
||||
const getAllPullRequests = useCallback(async () => {
|
||||
setLoading(true);
|
||||
|
||||
const teamPullRequests = await getPRsFromTeam(repositories);
|
||||
const teamPullRequests = await getPRsFromTeam(
|
||||
repositories,
|
||||
members,
|
||||
organization,
|
||||
);
|
||||
setPullRequests(formatPRsByReviewDecision(teamPullRequests));
|
||||
setLoading(false);
|
||||
}, [getPRsFromTeam, repositories]);
|
||||
}, [getPRsFromTeam, repositories, members, organization]);
|
||||
|
||||
useEffect(() => {
|
||||
getAllPullRequests();
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
/*
|
||||
* 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 { stringifyEntityRef } from '@backstage/catalog-model';
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
import { catalogApiRef, useEntity } from '@backstage/plugin-catalog-react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { getProjectNameFromEntity } from '../utils/functions';
|
||||
|
||||
export function useUserRepositories() {
|
||||
const { entity: teamEntity } = useEntity();
|
||||
const catalogApi = useApi(catalogApiRef);
|
||||
const [repositories, setRepositories] = useState<string[]>([]);
|
||||
|
||||
const getRepositoriesNames = useCallback(async () => {
|
||||
const entitiesList = await catalogApi.getEntities({
|
||||
filter: { 'relations.ownedBy': stringifyEntityRef(teamEntity) },
|
||||
});
|
||||
|
||||
const entitiesNames: string[] = entitiesList.items.map(componentEntity =>
|
||||
getProjectNameFromEntity(componentEntity),
|
||||
);
|
||||
|
||||
setRepositories([...new Set(entitiesNames)]);
|
||||
}, [catalogApi, teamEntity]);
|
||||
|
||||
useEffect(() => {
|
||||
getRepositoriesNames();
|
||||
}, [getRepositoriesNames]);
|
||||
|
||||
return {
|
||||
repositories,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* 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 { stringifyEntityRef } from '@backstage/catalog-model';
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
import { catalogApiRef, useEntity } from '@backstage/plugin-catalog-react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import {
|
||||
getProjectNameFromEntity,
|
||||
getGithubOrganizationFromEntity,
|
||||
getUserNameFromEntity,
|
||||
} from '../utils/functions';
|
||||
|
||||
export function useUserRepositoriesAndTeam() {
|
||||
const { entity: teamEntity } = useEntity();
|
||||
const catalogApi = useApi(catalogApiRef);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [teamData, setTeamData] = useState<{
|
||||
repositories: string[];
|
||||
teamMembers: string[];
|
||||
}>({
|
||||
repositories: [],
|
||||
teamMembers: [],
|
||||
});
|
||||
|
||||
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) },
|
||||
],
|
||||
});
|
||||
|
||||
const repositories = entitiesList.items.filter(
|
||||
entity => entity.kind === 'Component',
|
||||
);
|
||||
const repositoriesNames: string[] = repositories
|
||||
.map(componentEntity => getProjectNameFromEntity(componentEntity) ?? '')
|
||||
.filter(projectName => !!projectName);
|
||||
|
||||
const teamMembers = entitiesList.items.filter(
|
||||
entity => entity.kind === 'User',
|
||||
);
|
||||
const teamMembersNames: string[] = teamMembers
|
||||
.map(componentEntity => getUserNameFromEntity(componentEntity) ?? '')
|
||||
.filter(userName => !!userName);
|
||||
|
||||
setTeamData({
|
||||
repositories: [...new Set(repositoriesNames)],
|
||||
teamMembers: [...new Set(teamMembersNames)],
|
||||
});
|
||||
setLoading(false);
|
||||
}, [catalogApi, teamEntity]);
|
||||
|
||||
useEffect(() => {
|
||||
getTeamData();
|
||||
}, [getTeamData]);
|
||||
|
||||
return {
|
||||
loading,
|
||||
repositories: teamData.repositories,
|
||||
teamMembers: teamData.teamMembers,
|
||||
teamMembersOrganization: getGithubOrganizationFromEntity(teamEntity),
|
||||
};
|
||||
}
|
||||
@@ -21,13 +21,29 @@ import {
|
||||
ReviewDecision,
|
||||
PullRequestsColumn,
|
||||
Author,
|
||||
PRCardFormating,
|
||||
Repository,
|
||||
} from './types';
|
||||
import { COLUMNS } from './constants';
|
||||
|
||||
const GITHUB_PULL_REQUESTS_ANNOTATION = 'github.com/project-slug';
|
||||
const GITHUB_USER_LOGIN_ANNOTATION = 'github.com/user-login';
|
||||
|
||||
export const getProjectNameFromEntity = (entity: Entity): string => {
|
||||
return entity?.metadata.annotations?.[GITHUB_PULL_REQUESTS_ANNOTATION] ?? '';
|
||||
export const getProjectNameFromEntity = (
|
||||
entity: Entity,
|
||||
): string | undefined => {
|
||||
return entity?.metadata.annotations?.[GITHUB_PULL_REQUESTS_ANNOTATION];
|
||||
};
|
||||
|
||||
export const getUserNameFromEntity = (entity: Entity): string | undefined => {
|
||||
return entity?.metadata.annotations?.[GITHUB_USER_LOGIN_ANNOTATION];
|
||||
};
|
||||
|
||||
export const getGithubOrganizationFromEntity = (entity: Entity): string => {
|
||||
return (
|
||||
entity?.metadata?.annotations?.['github.com/team-slug']?.split('/')?.[0] ??
|
||||
''
|
||||
);
|
||||
};
|
||||
|
||||
export const getApprovedReviews = (reviews: Reviews = []): Reviews => {
|
||||
@@ -107,3 +123,32 @@ export const formatPRsByReviewDecision = (
|
||||
{ title: COLUMNS.APPROVED, content: reviewDecisions.APPROVED },
|
||||
];
|
||||
};
|
||||
|
||||
export const shouldDisplayCard = (
|
||||
repository: Repository,
|
||||
author: Author,
|
||||
teamRepositories: string[],
|
||||
teamMembers: string[],
|
||||
infoCardFormat: PRCardFormating[],
|
||||
isDraft: boolean,
|
||||
) => {
|
||||
// hide draft PRs unless "draft" filter is toggled
|
||||
if (infoCardFormat.includes('draft') !== isDraft) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// when "team" filter is toggled on, only shows PR from team members
|
||||
if (infoCardFormat.includes('team')) {
|
||||
return teamMembers.includes(author.login);
|
||||
}
|
||||
|
||||
const fullRepoName =
|
||||
`${repository.owner.login}/${repository.name}`.toLocaleLowerCase('en-US');
|
||||
|
||||
const repositories = teamRepositories.map(repo =>
|
||||
repo.toLocaleLowerCase('en-US'),
|
||||
);
|
||||
|
||||
// when "team" filter is toggled off, only shows PR on team repos
|
||||
return repositories.includes(fullRepoName);
|
||||
};
|
||||
|
||||
@@ -19,22 +19,33 @@ export type GraphQlPullRequest<T> = {
|
||||
};
|
||||
};
|
||||
|
||||
export type Connection<T> = {
|
||||
nodes: T;
|
||||
pageInfo: {
|
||||
hasNextPage: boolean;
|
||||
endCursor?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type GraphQlPullRequests<T> = {
|
||||
repository: {
|
||||
pullRequests: {
|
||||
edges: T;
|
||||
pageInfo: {
|
||||
hasNextPage: boolean;
|
||||
endCursor?: string;
|
||||
};
|
||||
};
|
||||
pullRequests: Connection<T>;
|
||||
};
|
||||
};
|
||||
|
||||
export type GraphQlUserPullRequests<T> = {
|
||||
user: {
|
||||
pullRequests: Connection<T>;
|
||||
};
|
||||
};
|
||||
|
||||
export type PullRequestsNumber = {
|
||||
node: {
|
||||
number: number;
|
||||
};
|
||||
number: number;
|
||||
};
|
||||
|
||||
export type PullRequestsNumberAndOwner = {
|
||||
number: number;
|
||||
repository: Repository;
|
||||
};
|
||||
|
||||
export type Review = {
|
||||
@@ -57,11 +68,16 @@ export type Author = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type Repository = {
|
||||
name: string;
|
||||
owner: {
|
||||
login: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type PullRequest = {
|
||||
id: string;
|
||||
repository: {
|
||||
name: string;
|
||||
};
|
||||
repository: Repository;
|
||||
title: string;
|
||||
url: string;
|
||||
lastEditedAt: string;
|
||||
@@ -83,6 +99,8 @@ export type PullRequestsColumn = {
|
||||
content: PullRequests;
|
||||
};
|
||||
|
||||
export type PRCardFormating = 'compacted' | 'fullscreen' | 'draft';
|
||||
export type PRCardFormating = 'compacted' | 'fullscreen' | 'draft' | 'team';
|
||||
|
||||
export type ReviewDecision = 'IN_PROGRESS' | 'APPROVED' | 'REVIEW_REQUIRED';
|
||||
|
||||
export type PullRequestsSourceType = 'TeamRepositories' | 'TeamMembers';
|
||||
|
||||
Reference in New Issue
Block a user