Merge pull request #8282 from marleypowell/marley/7678-pr-dashboard-backend

feat: Added new backend endpoints for PR dashboard and implemented long polling for PRs
This commit is contained in:
Patrik Oldsberg
2021-12-01 18:11:16 +01:00
committed by GitHub
15 changed files with 730 additions and 40 deletions
+17
View File
@@ -0,0 +1,17 @@
---
'@backstage/plugin-azure-devops': patch
'@backstage/plugin-azure-devops-backend': patch
---
**Backend**
- Created new `/dashboard-pull-requests/:projectName` endpoint
- Created new `/all-teams` endpoint
- Implemented pull request policy evaluation conversion
**Frontend**
- Refactored `PullRequestsPage` and added new properties for `projectName` and `pollingInterval`
- Fixed spacing issue between repo link and creation date in `PullRequestCard`
- Added missing condition to `PullRequestCardPolicy` for `RequiredReviewers`
- Updated `useDashboardPullRequests` hook to implement long polling for pull requests
@@ -5,12 +5,14 @@
```ts
import { Build } from 'azure-devops-node-api/interfaces/BuildInterfaces';
import { Config } from '@backstage/config';
import { DashboardPullRequest } from '@backstage/plugin-azure-devops-common';
import express from 'express';
import { GitRepository } from 'azure-devops-node-api/interfaces/GitInterfaces';
import { Logger as Logger_2 } from 'winston';
import { PullRequest } from '@backstage/plugin-azure-devops-common';
import { PullRequestOptions } from '@backstage/plugin-azure-devops-common';
import { RepoBuild } from '@backstage/plugin-azure-devops-common';
import { Team } from '@backstage/plugin-azure-devops-common';
import { WebApi } from 'azure-devops-node-api';
// Warning: (ae-missing-release-tag) "AzureDevOpsApi" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
@@ -19,12 +21,19 @@ import { WebApi } from 'azure-devops-node-api';
export class AzureDevOpsApi {
constructor(logger: Logger_2, webApi: WebApi);
// (undocumented)
getAllTeams(): Promise<Team[]>;
// (undocumented)
getBuildList(
projectName: string,
repoId: string,
top: number,
): Promise<Build[]>;
// (undocumented)
getDashboardPullRequests(
projectName: string,
options: PullRequestOptions,
): Promise<DashboardPullRequest[]>;
// (undocumented)
getGitRepository(
projectName: string,
repoName: string,
@@ -17,19 +17,30 @@
import {
BuildResult,
BuildStatus,
DashboardPullRequest,
Policy,
PullRequest,
PullRequestOptions,
RepoBuild,
Team,
} from '@backstage/plugin-azure-devops-common';
import {
GitPullRequest,
GitPullRequestSearchCriteria,
GitRepository,
} from 'azure-devops-node-api/interfaces/GitInterfaces';
import {
convertDashboardPullRequest,
convertPolicy,
getArtifactId,
} from '../utils';
import { Build } from 'azure-devops-node-api/interfaces/BuildInterfaces';
import { Logger } from 'winston';
import { PolicyEvaluationRecord } from 'azure-devops-node-api/interfaces/PolicyInterfaces';
import { TeamMember } from 'azure-devops-node-api/interfaces/common/VSSInterfaces';
import { WebApi } from 'azure-devops-node-api';
import { WebApiTeam } from 'azure-devops-node-api/interfaces/CoreInterfaces';
export class AzureDevOpsApi {
public constructor(
@@ -138,6 +149,115 @@ export class AzureDevOpsApi {
return pullRequests;
}
public async getDashboardPullRequests(
projectName: string,
options: PullRequestOptions,
): Promise<DashboardPullRequest[]> {
this.logger?.debug(
`Getting dashboard pull requests for project '${projectName}'.`,
);
const client = await this.webApi.getGitApi();
const searchCriteria: GitPullRequestSearchCriteria = {
status: options.status,
};
const gitPullRequests: GitPullRequest[] =
await client.getPullRequestsByProject(
projectName,
searchCriteria,
undefined,
undefined,
options.top,
);
return Promise.all(
gitPullRequests.map(async gitPullRequest => {
const projectId = gitPullRequest.repository?.project?.id;
const prId = gitPullRequest.pullRequestId;
let policies: Policy[] | undefined;
if (projectId && prId) {
policies = await this.getPullRequestPolicies(
projectName,
projectId,
prId,
);
}
return convertDashboardPullRequest(
gitPullRequest,
this.webApi.serverUrl,
policies,
);
}),
);
}
private async getPullRequestPolicies(
projectName: string,
projectId: string,
pullRequestId: number,
): Promise<Policy[]> {
this.logger?.debug(
`Getting pull request policies for pull request id '${pullRequestId}'.`,
);
const client = await this.webApi.getPolicyApi();
const artifactId = getArtifactId(projectId, pullRequestId);
const policyEvaluationRecords: PolicyEvaluationRecord[] =
await client.getPolicyEvaluations(projectName, artifactId);
return policyEvaluationRecords
.map(convertPolicy)
.filter((policy): policy is Policy => Boolean(policy));
}
public async getAllTeams(): Promise<Team[]> {
this.logger?.debug('Getting all teams.');
const client = await this.webApi.getCoreApi();
const webApiTeams: WebApiTeam[] = await client.getAllTeams();
const teams: Team[] = await Promise.all(
webApiTeams.map(async team => ({
id: team.id,
name: team.name,
memberIds: await this.getTeamMemberIds(team),
})),
);
return teams.sort((a, b) =>
a.name && b.name ? a.name.localeCompare(b.name) : 0,
);
}
private async getTeamMemberIds(
team: WebApiTeam,
): Promise<string[] | undefined> {
this.logger?.debug(`Getting team member ids for team '${team.name}'.`);
if (!team.projectId || !team.id) {
return undefined;
}
const client = await this.webApi.getCoreApi();
const teamMembers: TeamMember[] =
await client.getTeamMembersWithExtendedProperties(
team.projectId,
team.id,
);
return teamMembers
.map(teamMember => teamMember.identity?.id)
.filter((id): id is string => Boolean(id));
}
}
export function mappedRepoBuild(build: Build): RepoBuild {
@@ -15,6 +15,7 @@
*/
import {
DashboardPullRequest,
PullRequestOptions,
PullRequestStatus,
} from '@backstage/plugin-azure-devops-common';
@@ -27,7 +28,7 @@ import Router from 'express-promise-router';
import { errorHandler } from '@backstage/backend-common';
import express from 'express';
const DEFAULT_TOP: number = 10;
const DEFAULT_TOP = 10;
export interface RouterOptions {
azureDevOpsApi?: AzureDevOpsApi;
@@ -80,33 +81,69 @@ export async function createRouter(
router.get('/repo-builds/:projectName/:repoName', async (req, res) => {
const { projectName, repoName } = req.params;
const top = req.query.top ? Number(req.query.top) : DEFAULT_TOP;
const gitRepository = await azureDevOpsApi.getRepoBuilds(
projectName,
repoName,
top,
);
res.status(200).json(gitRepository);
});
router.get('/pull-requests/:projectName/:repoName', async (req, res) => {
const { projectName, repoName } = req.params;
const top = req.query.top ? Number(req.query.top) : DEFAULT_TOP;
const status = req.query.status
? Number(req.query.status)
: PullRequestStatus.Active;
const pullRequestOptions: PullRequestOptions = {
top: top,
status: status,
};
const gitPullRequest = await azureDevOpsApi.getPullRequests(
projectName,
repoName,
pullRequestOptions,
);
res.status(200).json(gitPullRequest);
});
router.get('/dashboard-pull-requests/:projectName', async (req, res) => {
const { projectName } = req.params;
const top = req.query.top ? Number(req.query.top) : DEFAULT_TOP;
const status = req.query.status
? Number(req.query.status)
: PullRequestStatus.Active;
const pullRequestOptions: PullRequestOptions = {
top: top,
status: status,
};
const pullRequests: DashboardPullRequest[] =
await azureDevOpsApi.getDashboardPullRequests(
projectName,
pullRequestOptions,
);
res.status(200).json(pullRequests);
});
router.get('/all-teams', async (_req, res) => {
const allTeams = await azureDevOpsApi.getAllTeams();
res.status(200).json(allTeams);
});
router.use(errorHandler());
return router;
}
@@ -0,0 +1,161 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
DashboardPullRequest,
PullRequestStatus,
PullRequestVoteStatus,
} from '@backstage/plugin-azure-devops-common';
import {
convertDashboardPullRequest,
getArtifactId,
getAvatarUrl,
getPullRequestLink,
} from './azure-devops-utils';
import { GitPullRequest } from 'azure-devops-node-api/interfaces/GitInterfaces';
describe('convertDashboardPullRequest', () => {
it('should return DashboardPullRequest', () => {
const baseUrl = 'https://dev.azure.com';
const pullRequest: GitPullRequest = {
pullRequestId: 1,
title: 'Pull Request 1',
description: 'Test description',
repository: {
id: 'repo1',
name: 'azure-devops',
url: 'https://dev.azure.com/backstage/backstage/_apis/git/repositories/azure-devops',
project: {
name: 'backstage',
},
},
createdBy: {
id: 'user1',
displayName: 'User 1',
uniqueName: 'user1@backstage.io',
_links: {
avatar: {
href: 'avatar-href',
},
},
imageUrl: 'avatar-url',
},
reviewers: [
{
id: 'user2',
displayName: 'User 2',
_links: {
avatar: {
href: 'avatar-href',
},
},
isRequired: true,
isContainer: false,
vote: 10,
},
],
creationDate: new Date('2021-10-15T09:30:00.0000000Z'),
status: PullRequestStatus.Active,
isDraft: false,
completionOptions: {},
};
const expectedPullRequest: DashboardPullRequest = {
pullRequestId: 1,
title: 'Pull Request 1',
description: 'Test description',
repository: {
id: 'repo1',
name: 'azure-devops',
url: 'https://dev.azure.com/backstage/backstage/_git/azure-devops',
},
createdBy: {
id: 'user1',
displayName: 'User 1',
uniqueName: 'user1@backstage.io',
imageUrl: 'avatar-href',
},
hasAutoComplete: true,
policies: [],
reviewers: [
{
id: 'user2',
displayName: 'User 2',
imageUrl: 'avatar-href',
isRequired: true,
isContainer: false,
voteStatus: PullRequestVoteStatus.Approved,
},
],
creationDate: '2021-10-15T09:30:00.000Z',
status: PullRequestStatus.Active,
isDraft: false,
link: 'https://dev.azure.com/backstage/_git/azure-devops/pullrequest/1',
};
const result = convertDashboardPullRequest(pullRequest, baseUrl, []);
expect(result).toEqual(expectedPullRequest);
});
});
describe('getPullRequestLink', () => {
it('should return pull request link', () => {
const baseUrl = 'dev.azure.com';
const pullRequest = {
pullRequestId: 1,
repository: {
name: 'azure-devops',
project: {
name: 'backstage',
},
},
};
const result = getPullRequestLink(baseUrl, pullRequest);
expect(result).toBe(`${baseUrl}/backstage/_git/azure-devops/pullrequest/1`);
});
});
describe('getAvatarUrl', () => {
it('should return avatar href', () => {
const identity = {
_links: {
avatar: {
href: 'avatar-href',
},
},
imageUrl: 'avatar-url',
};
const result = getAvatarUrl(identity);
expect(result).toBe('avatar-href');
});
it('should return avatar image url', () => {
const identity = {
imageUrl: 'avatar-url',
};
const result = getAvatarUrl(identity);
expect(result).toBe('avatar-url');
});
});
describe('getArtifactId', () => {
it('should return artifact id', () => {
const result = getArtifactId('project1', 1);
expect(result).toBe('vstfs:///CodeReview/CodeReviewId/project1/1');
});
});
@@ -0,0 +1,264 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
CreatedBy,
DashboardPullRequest,
Policy,
PolicyEvaluationStatus,
PolicyType,
PolicyTypeId,
PullRequestVoteStatus,
Repository,
Reviewer,
} from '@backstage/plugin-azure-devops-common';
import {
GitPullRequest,
GitRepository,
IdentityRefWithVote,
} from 'azure-devops-node-api/interfaces/GitInterfaces';
import { IdentityRef } from 'azure-devops-node-api/interfaces/common/VSSInterfaces';
import { PolicyEvaluationRecord } from 'azure-devops-node-api/interfaces/PolicyInterfaces';
export function convertDashboardPullRequest(
pullRequest: GitPullRequest,
baseUrl: string,
policies: Policy[] | undefined,
): DashboardPullRequest {
return {
pullRequestId: pullRequest.pullRequestId,
title: pullRequest.title,
description: pullRequest.description,
repository: convertRepository(pullRequest.repository),
createdBy: convertCreatedBy(pullRequest.createdBy),
hasAutoComplete: hasAutoComplete(pullRequest),
policies,
reviewers: convertReviewers(pullRequest.reviewers),
creationDate: pullRequest.creationDate?.toISOString(),
status: pullRequest.status,
isDraft: pullRequest.isDraft,
link: getPullRequestLink(baseUrl, pullRequest),
};
}
export function getPullRequestLink(
baseUrl: string,
pullRequest: GitPullRequest,
): string | undefined {
const projectName = pullRequest.repository?.project?.name;
const repoName = pullRequest.repository?.name;
const pullRequestId = pullRequest.pullRequestId;
if (!projectName || !repoName || !pullRequestId) {
return undefined;
}
const encodedProjectName = encodeURIComponent(projectName);
const encodedRepoName = encodeURIComponent(repoName);
return `${baseUrl}/${encodedProjectName}/_git/${encodedRepoName}/pullrequest/${pullRequestId}`;
}
/**
* Tries to get the avatar from the new property if not then falls-back to deprecated `imageUrl`.
* https://docs.microsoft.com/en-us/rest/api/azure/devops/git/pull-requests/get-pull-requests-by-project?view=azure-devops-rest-6.0#identityref
*/
export function getAvatarUrl(identity: IdentityRef): string | undefined {
return identity._links?.avatar?.href ?? identity.imageUrl;
}
export function getArtifactId(
projectId: string,
pullRequestId: number,
): string {
return `vstfs:///CodeReview/CodeReviewId/${projectId}/${pullRequestId}`;
}
export function convertPolicy(
policyEvaluationRecord: PolicyEvaluationRecord,
): Policy | undefined {
const policyConfig = policyEvaluationRecord.configuration;
const policyStatus = policyEvaluationRecord.status;
if (!policyConfig) {
return undefined;
}
if (
!(
policyConfig.isEnabled &&
!policyConfig.isDeleted &&
(policyConfig.isBlocking ||
policyConfig.type?.id === PolicyType.Status) && // Optional "Status" policies are actually required for automatic completion.
policyStatus !== PolicyEvaluationStatus.Approved
)
) {
return undefined;
}
const policyTypeId = policyConfig.type?.id;
if (!policyTypeId) {
return undefined;
}
const policyType: PolicyType | undefined = (
{
[PolicyTypeId.Build]: PolicyType.Build,
[PolicyTypeId.Status]: PolicyType.Status,
[PolicyTypeId.MinimumReviewers]: PolicyType.MinimumReviewers,
[PolicyTypeId.Comments]: PolicyType.Comments,
[PolicyTypeId.RequiredReviewers]: PolicyType.RequiredReviewers,
[PolicyTypeId.MergeStrategy]: PolicyType.MergeStrategy,
} as Record<string, PolicyType | undefined>
)[policyTypeId];
if (!policyType) {
return undefined;
}
const policyConfigSettings = policyConfig.settings;
let policyText = policyConfig.type?.displayName;
let policyLink: string | undefined;
switch (policyType) {
case PolicyType.Build: {
const buildDisplayName = policyConfigSettings.displayName;
if (buildDisplayName) {
policyText += `: ${buildDisplayName}`;
}
const buildId = policyEvaluationRecord.context?.buildId;
const policyConfigUrl = policyConfig.url;
if (buildId && policyConfigUrl) {
policyLink = policyConfigUrl.replace(
`_apis/policy/configurations/${policyConfig.id}`,
`_build/results?buildId=${buildId}`,
);
}
if (!policyStatus) {
break;
}
const buildExpired = Boolean(policyConfigSettings.isExpired);
const buildPolicyStatus =
(
{
[PolicyEvaluationStatus.Queued]: buildExpired
? 'expired'
: 'queued',
[PolicyEvaluationStatus.Rejected]: 'failed',
} as Record<PolicyEvaluationStatus, string | undefined>
)[policyStatus] ?? PolicyEvaluationStatus[policyStatus].toLowerCase();
policyText += ` (${buildPolicyStatus})`;
break;
}
case PolicyType.Status: {
const statusGenre = policyConfigSettings.statusGenre;
const statusName = policyConfigSettings.statusGenre;
if (statusName) {
policyText += `: ${statusGenre}/${statusName}`;
}
break;
}
case PolicyType.MinimumReviewers: {
const minimumApproverCount = policyConfigSettings.minimumApproverCount;
policyText += ` (${minimumApproverCount})`;
break;
}
case PolicyType.Comments:
break;
case PolicyType.RequiredReviewers:
break;
case PolicyType.MergeStrategy:
default:
return undefined;
}
return {
id: policyConfig.id,
type: policyType,
status: policyStatus,
text: policyText,
link: policyLink,
};
}
function convertReviewer(
identityRef?: IdentityRefWithVote,
): Reviewer | undefined {
if (!identityRef) {
return undefined;
}
return {
id: identityRef.id,
displayName: identityRef.displayName,
imageUrl: getAvatarUrl(identityRef),
isRequired: identityRef.isRequired,
isContainer: identityRef.isContainer,
voteStatus: (identityRef.vote ?? 0) as PullRequestVoteStatus,
};
}
function convertReviewers(
identityRefs?: IdentityRefWithVote[],
): Reviewer[] | undefined {
if (!identityRefs) {
return undefined;
}
return identityRefs
.map(convertReviewer)
.filter((reviewer): reviewer is Reviewer => Boolean(reviewer));
}
function convertRepository(repository?: GitRepository): Repository | undefined {
if (!repository) {
return undefined;
}
return {
id: repository.id,
name: repository.name,
url: repository.url?.replace('_apis/git/repositories', '_git'),
};
}
function convertCreatedBy(identityRef?: IdentityRef): CreatedBy | undefined {
if (!identityRef) {
return undefined;
}
return {
id: identityRef.id,
displayName: identityRef.displayName,
uniqueName: identityRef.uniqueName,
imageUrl: getAvatarUrl(identityRef),
};
}
function hasAutoComplete(pullRequest: GitPullRequest): boolean {
return pullRequest.isDraft !== true && !!pullRequest.completionOptions;
}
@@ -0,0 +1,17 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './azure-devops-utils';
+8 -8
View File
@@ -120,14 +120,14 @@ export enum PolicyType {
// Warning: (ae-missing-release-tag) "PolicyTypeId" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const PolicyTypeId: {
Build: string;
Status: string;
MinimumReviewers: string;
Comments: string;
RequiredReviewers: string;
MergeStrategy: string;
};
export enum PolicyTypeId {
Build = '0609b952-1397-4640-95ec-e00a01b2c241',
Comments = 'c6a1889d-b943-4856-b76f-9e46bb6b0df2',
MergeStrategy = 'fa4e907d-c16b-4a4c-9dfa-4916e5d171ab',
MinimumReviewers = 'fa4e907d-c16b-4a4c-9dfa-4906e5d171dd',
RequiredReviewers = 'fd2167ab-b0be-447a-8ec8-39368250530e',
Status = 'cbdc66da-9728-4af8-aada-9a5a32e4a226',
}
// Warning: (ae-missing-release-tag) "PullRequest" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
+8 -8
View File
@@ -217,32 +217,32 @@ export enum PolicyType {
MergeStrategy = 'MergeStrategy',
}
export const PolicyTypeId = {
export enum PolicyTypeId {
/**
* This policy will require a successful build has been performed before updating protected refs.
*/
Build: '0609b952-1397-4640-95ec-e00a01b2c241',
Build = '0609b952-1397-4640-95ec-e00a01b2c241',
/**
* This policy will require a successful status to be posted before updating protected refs.
*/
Status: 'cbdc66da-9728-4af8-aada-9a5a32e4a226',
Status = 'cbdc66da-9728-4af8-aada-9a5a32e4a226',
/**
* This policy will ensure that a minimum number of reviewers have approved a pull request before completion.
*/
MinimumReviewers: 'fa4e907d-c16b-4a4c-9dfa-4906e5d171dd',
MinimumReviewers = 'fa4e907d-c16b-4a4c-9dfa-4906e5d171dd',
/**
* Check if the pull request has any active comments.
*/
Comments: 'c6a1889d-b943-4856-b76f-9e46bb6b0df2',
Comments = 'c6a1889d-b943-4856-b76f-9e46bb6b0df2',
/**
* This policy will ensure that required reviewers are added for modified files matching specified patterns.
*/
RequiredReviewers: 'fd2167ab-b0be-447a-8ec8-39368250530e',
RequiredReviewers = 'fd2167ab-b0be-447a-8ec8-39368250530e',
/**
* This policy ensures that pull requests use a consistent merge strategy.
*/
MergeStrategy: 'fa4e907d-c16b-4a4c-9dfa-4916e5d171ab',
};
MergeStrategy = 'fa4e907d-c16b-4a4c-9dfa-4916e5d171ab',
}
export enum PullRequestVoteStatus {
Approved = 10,
+7 -1
View File
@@ -22,7 +22,13 @@ export const AzurePullRequestsIcon: (props: SvgIconProps) => JSX.Element;
// Warning: (ae-missing-release-tag) "AzurePullRequestsPage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const AzurePullRequestsPage: () => JSX.Element;
export const AzurePullRequestsPage: ({
projectName,
pollingInterval,
}: {
projectName?: string | undefined;
pollingInterval?: number | undefined;
}) => JSX.Element;
// Warning: (ae-missing-release-tag) "EntityAzurePipelinesContent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
@@ -18,6 +18,7 @@ import {
Content,
Header,
Page,
Progress,
ResponseErrorPanel,
} from '@backstage/core-components';
import { PullRequestGroup, PullRequestGroupConfig } from './lib/types';
@@ -28,11 +29,6 @@ import { useDashboardPullRequests, useUserEmail } from '../../hooks';
import { DashboardPullRequest } from '@backstage/plugin-azure-devops-common';
import { PullRequestGrid } from './lib/PullRequestGrid';
/**
* @deprecated TEMPORARY - This will be configurable in a follow up PR.
*/
const PROJECT_NAME = 'projectName';
function usePullRequestGroupConfigs(
userEmail: string | undefined,
): PullRequestGroupConfig[] {
@@ -73,8 +69,41 @@ function usePullRequestGroups(
return pullRequestGroups;
}
export const PullRequestsPage = () => {
const { pullRequests, error } = useDashboardPullRequests(PROJECT_NAME);
type PullRequestsPageContentProps = {
pullRequestGroups: PullRequestGroup[];
loading: boolean;
error?: Error;
};
const PullRequestsPageContent = ({
pullRequestGroups,
loading,
error,
}: PullRequestsPageContentProps) => {
if (loading && pullRequestGroups.length <= 0) {
return <Progress />;
}
if (error) {
return <ResponseErrorPanel error={error} />;
}
return <PullRequestGrid pullRequestGroups={pullRequestGroups} />;
};
type PullRequestsPageProps = {
projectName?: string;
pollingInterval?: number;
};
export const PullRequestsPage = ({
projectName,
pollingInterval,
}: PullRequestsPageProps) => {
const { pullRequests, loading, error } = useDashboardPullRequests(
projectName,
pollingInterval,
);
const userEmail = useUserEmail();
const pullRequestGroupConfigs = usePullRequestGroupConfigs(userEmail);
const pullRequestGroups = usePullRequestGroups(
@@ -82,16 +111,16 @@ export const PullRequestsPage = () => {
pullRequestGroupConfigs,
);
const pullRequestsContent = error ? (
<ResponseErrorPanel error={error} />
) : (
<PullRequestGrid pullRequestGroups={pullRequestGroups} />
);
return (
<Page themeId="tool">
<Header title="Azure Pull Requests" />
<Content>{pullRequestsContent}</Content>
<Content>
<PullRequestsPageContent
pullRequestGroups={pullRequestGroups}
loading={loading}
error={error}
/>
</Content>
</Page>
);
};
@@ -79,7 +79,7 @@ export const PullRequestCard = ({
const subheader = (
<span>
{repoLink}·{creationDate}
{repoLink} · {creationDate}
</span>
);
@@ -67,6 +67,7 @@ function getPolicyIcon(policy: Policy): JSX.Element | null {
return null;
}
case PolicyType.MinimumReviewers:
case PolicyType.RequiredReviewers:
return <PolicyRequiredIcon />;
case PolicyType.Status:
case PolicyType.Comments:
@@ -14,25 +14,55 @@
* limitations under the License.
*/
import { errorApiRef, useApi } from '@backstage/core-plugin-api';
import { useAsyncRetry, useInterval } from 'react-use';
import { DashboardPullRequest } from '@backstage/plugin-azure-devops-common';
import { azureDevOpsApiRef } from '../api';
import { useApi } from '@backstage/core-plugin-api';
import { useAsync } from 'react-use';
import { useCallback } from 'react';
export function useDashboardPullRequests(project: string): {
const POLLING_INTERVAL = 10000;
export function useDashboardPullRequests(
project?: string,
pollingInterval: number = POLLING_INTERVAL,
): {
pullRequests?: DashboardPullRequest[];
loading: boolean;
error?: Error;
} {
const api = useApi(azureDevOpsApiRef);
const errorApi = useApi(errorApiRef);
const getDashboardPullRequests = useCallback(async (): Promise<
DashboardPullRequest[]
> => {
if (!project) {
return Promise.reject(new Error('Missing project name'));
}
try {
return await api.getDashboardPullRequests(project);
} catch (error) {
if (error instanceof Error) {
errorApi.post(error);
}
return Promise.reject(error);
}
}, [project, api, errorApi]);
const {
value: pullRequests,
loading,
error,
} = useAsync(() => {
return api.getDashboardPullRequests(project);
}, [api, project]);
retry,
} = useAsyncRetry(
() => getDashboardPullRequests(),
[getDashboardPullRequests],
);
useInterval(() => retry(), pollingInterval);
return {
pullRequests,
-1
View File
@@ -18,7 +18,6 @@ import { createRouteRef } from '@backstage/core-plugin-api';
export const azurePullRequestDashboardRouteRef = createRouteRef({
id: 'azure-pull-request-dashboard',
path: '',
});
export const azurePipelinesEntityContentRouteRef = createRouteRef({