Merge pull request #7897 from marleypowell/marley/7678-pull-request-dashboard

feat: Created pull request card component and initial pull request dashboard page
This commit is contained in:
Patrik Oldsberg
2021-11-30 22:07:21 +01:00
committed by GitHub
33 changed files with 1605 additions and 2 deletions
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/plugin-azure-devops': patch
'@backstage/plugin-azure-devops-common': patch
---
feat: Created pull request card component and initial pull request dashboard page.
+2
View File
@@ -34,6 +34,7 @@ import {
SignInPage,
} from '@backstage/core-components';
import { apiDocsPlugin, ApiExplorerPage } from '@backstage/plugin-api-docs';
import { AzurePullRequestsPage } from '@backstage/plugin-azure-devops';
import {
CatalogEntityPage,
CatalogIndexPage,
@@ -216,6 +217,7 @@ const routes = (
element={<CostInsightsLabelDataflowInstructionsPage />}
/>
<Route path="/settings" element={<UserSettingsPage />} />
<Route path="/azure-pull-requests" element={<AzurePullRequestsPage />} />
</FlatRoutes>
);
@@ -41,6 +41,7 @@ import {
SidebarSpace,
SidebarScrollWrapper,
} from '@backstage/core-components';
import { AzurePullRequestsIcon } from '@backstage/plugin-azure-devops';
const useSidebarLogoStyles = makeStyles({
root: {
@@ -94,6 +95,11 @@ export const Root = ({ children }: PropsWithChildren<{}>) => (
<SidebarItem icon={RuleIcon} to="lighthouse" text="Lighthouse" />
<SidebarItem icon={MoneyIcon} to="cost-insights" text="Cost Insights" />
<SidebarItem icon={GraphiQLIcon} to="graphiql" text="GraphiQL" />
<SidebarItem
icon={AzurePullRequestsIcon}
to="azure-pull-requests"
text="Azure PRs"
/>
</SidebarScrollWrapper>
<SidebarDivider />
<Shortcuts />
+160
View File
@@ -27,6 +27,108 @@ export enum BuildStatus {
Postponed = 8,
}
// Warning: (ae-missing-release-tag) "CreatedBy" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export interface CreatedBy {
// (undocumented)
displayName?: string;
// (undocumented)
id?: string;
// (undocumented)
imageUrl?: string;
// (undocumented)
uniqueName?: string;
}
// Warning: (ae-missing-release-tag) "DashboardPullRequest" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export interface DashboardPullRequest {
// (undocumented)
createdBy?: CreatedBy;
// (undocumented)
creationDate?: string;
// (undocumented)
description?: string;
// (undocumented)
hasAutoComplete: boolean;
// (undocumented)
isDraft?: boolean;
// (undocumented)
link?: string;
// (undocumented)
policies?: Policy[];
// (undocumented)
pullRequestId?: number;
// (undocumented)
repository?: Repository;
// (undocumented)
reviewers?: Reviewer[];
// (undocumented)
status?: PullRequestStatus;
// (undocumented)
title?: string;
}
// Warning: (ae-missing-release-tag) "Policy" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export interface Policy {
// (undocumented)
id?: number;
// (undocumented)
link?: string;
// (undocumented)
status?: PolicyEvaluationStatus;
// (undocumented)
text?: string;
// (undocumented)
type: PolicyType;
}
// Warning: (ae-missing-release-tag) "PolicyEvaluationStatus" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
export enum PolicyEvaluationStatus {
Approved = 2,
Broken = 5,
NotApplicable = 4,
Queued = 0,
Rejected = 3,
Running = 1,
}
// Warning: (ae-missing-release-tag) "PolicyType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export enum PolicyType {
// (undocumented)
Build = 'Build',
// (undocumented)
Comments = 'Comments',
// (undocumented)
MergeStrategy = 'MergeStrategy',
// (undocumented)
MinimumReviewers = 'MinimumReviewers',
// (undocumented)
RequiredReviewers = 'RequiredReviewers',
// (undocumented)
Status = 'Status',
}
// 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;
};
// Warning: (ae-missing-release-tag) "PullRequest" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@@ -63,6 +165,22 @@ export enum PullRequestStatus {
NotSet = 0,
}
// Warning: (ae-missing-release-tag) "PullRequestVoteStatus" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export enum PullRequestVoteStatus {
// (undocumented)
Approved = 10,
// (undocumented)
ApprovedWithSuggestions = 5,
// (undocumented)
NoVote = 0,
// (undocumented)
Rejected = -10,
// (undocumented)
WaitingForAuthor = -5,
}
// Warning: (ae-missing-release-tag) "RepoBuild" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@@ -86,5 +204,47 @@ export type RepoBuildOptions = {
top?: number;
};
// Warning: (ae-missing-release-tag) "Repository" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export interface Repository {
// (undocumented)
id?: string;
// (undocumented)
name?: string;
// (undocumented)
url?: string;
}
// Warning: (ae-missing-release-tag) "Reviewer" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export interface Reviewer {
// (undocumented)
displayName?: string;
// (undocumented)
id?: string;
// (undocumented)
imageUrl?: string;
// (undocumented)
isContainer?: boolean;
// (undocumented)
isRequired?: boolean;
// (undocumented)
voteStatus: PullRequestVoteStatus;
}
// Warning: (ae-missing-release-tag) "Team" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export interface Team {
// (undocumented)
id?: string;
// (undocumented)
memberIds?: string[];
// (undocumented)
name?: string;
}
// (No @packageDocumentation comment for this package)
```
+125
View File
@@ -126,3 +126,128 @@ export type PullRequestOptions = {
top: number;
status: PullRequestStatus;
};
export interface DashboardPullRequest {
pullRequestId?: number;
title?: string;
description?: string;
repository?: Repository;
createdBy?: CreatedBy;
hasAutoComplete: boolean;
policies?: Policy[];
reviewers?: Reviewer[];
creationDate?: string;
status?: PullRequestStatus;
isDraft?: boolean;
link?: string;
}
export interface Reviewer {
id?: string;
displayName?: string;
imageUrl?: string;
isRequired?: boolean;
isContainer?: boolean;
voteStatus: PullRequestVoteStatus;
}
export interface Policy {
id?: number;
type: PolicyType;
status?: PolicyEvaluationStatus;
text?: string;
link?: string;
}
export interface CreatedBy {
id?: string;
displayName?: string;
uniqueName?: string;
imageUrl?: string;
}
export interface Repository {
id?: string;
name?: string;
url?: string;
}
export interface Team {
id?: string;
name?: string;
memberIds?: string[];
}
/**
* Status of a policy which is running against a specific pull request.
*/
export enum PolicyEvaluationStatus {
/**
* The policy is either queued to run, or is waiting for some event before progressing.
*/
Queued = 0,
/**
* The policy is currently running.
*/
Running = 1,
/**
* The policy has been fulfilled for this pull request.
*/
Approved = 2,
/**
* The policy has rejected this pull request.
*/
Rejected = 3,
/**
* The policy does not apply to this pull request.
*/
NotApplicable = 4,
/**
* The policy has encountered an unexpected error.
*/
Broken = 5,
}
export enum PolicyType {
Build = 'Build',
Status = 'Status',
MinimumReviewers = 'MinimumReviewers',
Comments = 'Comments',
RequiredReviewers = 'RequiredReviewers',
MergeStrategy = 'MergeStrategy',
}
export const PolicyTypeId = {
/**
* This policy will require a successful build has been performed before updating protected refs.
*/
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',
/**
* This policy will ensure that a minimum number of reviewers have approved a pull request before completion.
*/
MinimumReviewers: 'fa4e907d-c16b-4a4c-9dfa-4906e5d171dd',
/**
* Check if the pull request has any active comments.
*/
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',
/**
* This policy ensures that pull requests use a consistent merge strategy.
*/
MergeStrategy: 'fa4e907d-c16b-4a4c-9dfa-4916e5d171ab',
};
export enum PullRequestVoteStatus {
Approved = 10,
ApprovedWithSuggestions = 5,
NoVote = 0,
WaitingForAuthor = -5,
Rejected = -10,
}
+11
View File
@@ -7,12 +7,23 @@
import { BackstagePlugin } from '@backstage/core-plugin-api';
import { Entity } from '@backstage/catalog-model';
import { SvgIconProps } from '@material-ui/core';
// Warning: (ae-missing-release-tag) "azureDevOpsPlugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const azureDevOpsPlugin: BackstagePlugin<{}, {}>;
// Warning: (ae-missing-release-tag) "AzurePullRequestsIcon" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
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;
// Warning: (ae-missing-release-tag) "EntityAzurePipelinesContent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@@ -15,10 +15,12 @@
*/
import {
DashboardPullRequest,
PullRequest,
PullRequestOptions,
RepoBuild,
RepoBuildOptions,
Team,
} from '@backstage/plugin-azure-devops-common';
import { createApiRef } from '@backstage/core-plugin-api';
@@ -41,4 +43,10 @@ export interface AzureDevOpsApi {
repoName: string,
options?: PullRequestOptions,
): Promise<{ items: PullRequest[] }>;
getDashboardPullRequests(
projectName: string,
): Promise<DashboardPullRequest[]>;
getAllTeams(): Promise<Team[]>;
}
@@ -14,13 +14,15 @@
* limitations under the License.
*/
import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api';
import {
DashboardPullRequest,
PullRequest,
PullRequestOptions,
RepoBuild,
RepoBuildOptions,
Team,
} from '@backstage/plugin-azure-devops-common';
import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api';
import { AzureDevOpsApi } from './AzureDevOpsApi';
import { ResponseError } from '@backstage/errors';
@@ -29,7 +31,7 @@ export class AzureDevOpsClient implements AzureDevOpsApi {
private readonly discoveryApi: DiscoveryApi;
private readonly identityApi: IdentityApi;
constructor(options: {
public constructor(options: {
discoveryApi: DiscoveryApi;
identityApi: IdentityApi;
}) {
@@ -74,6 +76,18 @@ export class AzureDevOpsClient implements AzureDevOpsApi {
return { items };
}
public getDashboardPullRequests(
projectName: string,
): Promise<DashboardPullRequest[]> {
return this.get<DashboardPullRequest[]>(
`dashboard-pull-requests/${projectName}?top=100`,
);
}
public getAllTeams(): Promise<Team[]> {
return this.get<Team[]>('all-teams');
}
private async get<T>(path: string): Promise<T> {
const baseUrl = `${await this.discoveryApi.getBaseUrl('azure-devops')}/`;
const url = new URL(path, baseUrl);
@@ -0,0 +1,97 @@
/*
* 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 {
Content,
Header,
Page,
ResponseErrorPanel,
} from '@backstage/core-components';
import { PullRequestGroup, PullRequestGroupConfig } from './lib/types';
import React, { useEffect, useState } from 'react';
import { getCreatedByUserFilter, getPullRequestGroups } from './lib/utils';
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[] {
const [pullRequestGroupConfigs, setPullRequestGroupConfigs] = useState<
PullRequestGroupConfig[]
>([]);
useEffect(() => {
const prGroupConfigs: PullRequestGroupConfig[] = [
{ title: 'Created by me', filter: getCreatedByUserFilter(userEmail) },
{ title: 'Other PRs', filter: _ => true, simplified: false },
];
setPullRequestGroupConfigs(prGroupConfigs);
}, [userEmail]);
return pullRequestGroupConfigs;
}
function usePullRequestGroups(
pullRequests: DashboardPullRequest[] | undefined,
pullRequestGroupConfigs: PullRequestGroupConfig[],
): PullRequestGroup[] {
const [pullRequestGroups, setPullRequestGroups] = useState<
PullRequestGroup[]
>([]);
useEffect(() => {
if (pullRequests) {
const groups = getPullRequestGroups(
pullRequests,
pullRequestGroupConfigs,
);
setPullRequestGroups(groups);
}
}, [pullRequests, pullRequestGroupConfigs]);
return pullRequestGroups;
}
export const PullRequestsPage = () => {
const { pullRequests, error } = useDashboardPullRequests(PROJECT_NAME);
const userEmail = useUserEmail();
const pullRequestGroupConfigs = usePullRequestGroupConfigs(userEmail);
const pullRequestGroups = usePullRequestGroups(
pullRequests,
pullRequestGroupConfigs,
);
const pullRequestsContent = error ? (
<ResponseErrorPanel error={error} />
) : (
<PullRequestGrid pullRequestGroups={pullRequestGroups} />
);
return (
<Page themeId="tool">
<Header title="Azure Pull Requests" />
<Content>{pullRequestsContent}</Content>
</Page>
);
};
@@ -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 { PullRequestsPage } from './PullRequestsPage';
@@ -0,0 +1,32 @@
/*
* 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 DoneAllIcon from '@material-ui/icons/DoneAll';
import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
const useStyles = makeStyles(theme => ({
root: (props: { hasAutoComplete: boolean }) => ({
color: props.hasAutoComplete
? theme.palette.success.main
: theme.palette.grey[400],
}),
}));
export const AutoCompleteIcon = (props: { hasAutoComplete: boolean }) => {
const classes = useStyles(props);
return <DoneAllIcon className={classes.root} />;
};
@@ -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 { AutoCompleteIcon } from './AutoCompleteIcon';
@@ -0,0 +1,118 @@
/*
* 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,
PolicyEvaluationStatus,
PolicyType,
PullRequestStatus,
PullRequestVoteStatus,
} from '@backstage/plugin-azure-devops-common';
import { MemoryRouter } from 'react-router';
import { PullRequestCard } from './PullRequestCard';
import React from 'react';
export default {
title: 'Plugins/Azure Devops/Pull Request Card',
component: PullRequestCard,
};
const pullRequest: DashboardPullRequest = {
pullRequestId: 1,
title:
"feat(EXUX-4091): 🛂 Added the admin role authorization to the backend API's",
description:
'This PR contains the following updates:\n\n| Package | Type | Update | Change |\n|---|---|---|---|\n| [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint) | devDependencies | major | [`4.33.0` -> `5.0.0`](https://renovatebot.com/diffs/npm/@typescript-eslint%2feslint-plugin/4.33.0/5.0.0) |\n| [@typescript-eslint/parser](https://github.com/typescript-eslint/typescrip',
link: undefined,
repository: {
id: undefined,
name: 'backstage',
url: undefined,
},
createdBy: {
id: '',
displayName: 'Marley',
uniqueName: 'marley@test.com',
imageUrl:
'https://dev.azure.com/exclaimerltd/_api/_common/identityImage?id=e6c0634b-68d2-6e6f-aa7d-adccada23216',
},
reviewers: [
{
id: undefined,
displayName: 'Marley',
imageUrl: '',
isRequired: true,
isContainer: false,
voteStatus: PullRequestVoteStatus.Approved,
},
{
id: undefined,
displayName: 'User 1',
imageUrl: '',
isRequired: false,
isContainer: false,
voteStatus: PullRequestVoteStatus.WaitingForAuthor,
},
{
id: undefined,
displayName: 'User 2',
imageUrl: '',
isRequired: true,
isContainer: false,
voteStatus: PullRequestVoteStatus.NoVote,
},
],
policies: [
{
id: undefined,
type: PolicyType.Build,
status: PolicyEvaluationStatus.Running,
text: 'Build: UI (running)',
link: undefined,
},
{
id: undefined,
type: PolicyType.MinimumReviewers,
text: 'Minimum number of reviewers (2)',
status: undefined,
link: undefined,
},
{
id: undefined,
type: PolicyType.Comments,
text: 'Comment requirements',
status: undefined,
link: undefined,
},
],
hasAutoComplete: true,
creationDate: new Date(Date.now() - 10000000).toISOString(),
status: PullRequestStatus.Active,
isDraft: false,
};
export const Default = () => (
<MemoryRouter>
<PullRequestCard pullRequest={pullRequest} />
</MemoryRouter>
);
export const Simplified = () => (
<MemoryRouter>
<PullRequestCard pullRequest={pullRequest} simplified />
</MemoryRouter>
);
@@ -0,0 +1,130 @@
/*
* 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 { Avatar, Link } from '@backstage/core-components';
import { Card, CardContent, CardHeader } from '@material-ui/core';
import { AutoCompleteIcon } from '../AutoCompleteIcon';
import { DashboardPullRequest } from '@backstage/plugin-azure-devops-common';
import { DateTime } from 'luxon';
import { PullRequestCardPolicies } from './PullRequestCardPolicies';
import { PullRequestCardReviewers } from './PullRequestCardReviewers';
import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
const useStyles = makeStyles(
theme => ({
card: {
backgroundColor:
theme.palette.type === 'dark'
? theme.palette.grey[700]
: theme.palette.common.white,
},
cardHeaderSimplified: {
paddingBottom: theme.spacing(2),
},
cardHeaderAction: {
display: 'flex',
alignSelf: 'center',
margin: 0,
},
content: {
display: 'flex',
flexDirection: 'row',
},
policies: {
flex: 1,
},
}),
{ name: 'PullRequestCard' },
);
type PullRequestCardProps = {
pullRequest: DashboardPullRequest;
simplified?: boolean;
};
export const PullRequestCard = ({
pullRequest,
simplified,
}: PullRequestCardProps) => {
const title = (
<Link to={pullRequest.link ?? ''} title={pullRequest.description}>
{pullRequest.title}
</Link>
);
const repoLink = (
<Link to={pullRequest.repository?.url ?? ''} color="inherit">
{pullRequest.repository?.name}
</Link>
);
const creationDate = pullRequest.creationDate
? DateTime.fromISO(pullRequest.creationDate).toRelative()
: undefined;
const subheader = (
<span>
{repoLink}·{creationDate}
</span>
);
const avatar = (
<Avatar
displayName={pullRequest.createdBy?.displayName}
picture={pullRequest.createdBy?.imageUrl}
customStyles={{ width: '2.5rem', height: '2.5rem', fontSize: '1rem' }}
/>
);
const classes = useStyles();
return (
<Card
classes={{ root: classes.card }}
data-pull-request-id={pullRequest.pullRequestId}
>
<CardHeader
avatar={avatar}
title={title}
subheader={subheader}
action={
<AutoCompleteIcon hasAutoComplete={pullRequest.hasAutoComplete} />
}
classes={{
...(simplified && { root: classes.cardHeaderSimplified }),
action: classes.cardHeaderAction,
}}
/>
{!simplified && (
<CardContent className={classes.content}>
{pullRequest.policies && (
<PullRequestCardPolicies
policies={pullRequest.policies}
className={classes.policies}
/>
)}
{pullRequest.reviewers && (
<PullRequestCardReviewers reviewers={pullRequest.reviewers} />
)}
</CardContent>
)}
</Card>
);
};
@@ -0,0 +1,35 @@
/*
* 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 { Policy } from '@backstage/plugin-azure-devops-common';
import { PullRequestCardPolicy } from './PullRequestCardPolicy';
import React from 'react';
type PullRequestCardProps = {
policies: Policy[];
className: string;
};
export const PullRequestCardPolicies = ({
policies,
className,
}: PullRequestCardProps) => (
<div className={className}>
{policies.map(policy => (
<PullRequestCardPolicy key={policy.id} policy={policy} />
))}
</div>
);
@@ -0,0 +1,95 @@
/*
* 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 {
Policy,
PolicyEvaluationStatus,
PolicyType,
} from '@backstage/plugin-azure-devops-common';
import { styled, withStyles } from '@material-ui/core/styles';
import CancelIcon from '@material-ui/icons/Cancel';
import GroupWorkIcon from '@material-ui/icons/GroupWork';
import React from 'react';
import WatchLaterIcon from '@material-ui/icons/WatchLater';
const PolicyRequiredIcon = withStyles(
theme => ({
root: {
color: theme.palette.info.main,
},
}),
{ name: 'PolicyRequiredIcon' },
)(WatchLaterIcon);
const PolicyIssueIcon = withStyles(
theme => ({
root: {
color: theme.palette.error.main,
},
}),
{ name: 'PolicyIssueIcon' },
)(CancelIcon);
const PolicyInProgressIcon = withStyles(
theme => ({
root: {
color: theme.palette.info.main,
},
}),
{ name: 'PolicyInProgressIcon' },
)(GroupWorkIcon);
function getPolicyIcon(policy: Policy): JSX.Element | null {
switch (policy.type) {
case PolicyType.Build:
switch (policy.status) {
case PolicyEvaluationStatus.Running:
return <PolicyInProgressIcon />;
case PolicyEvaluationStatus.Rejected:
return <PolicyIssueIcon />;
case PolicyEvaluationStatus.Queued:
return <PolicyRequiredIcon />;
default:
return null;
}
case PolicyType.MinimumReviewers:
return <PolicyRequiredIcon />;
case PolicyType.Status:
case PolicyType.Comments:
return <PolicyIssueIcon />;
default:
return null;
}
}
const PullRequestCardPolicyContainer = styled('div')({
display: 'flex',
alignItems: 'center',
flexWrap: 'wrap',
});
type PullRequestCardPolicyProps = {
policy: Policy;
};
export const PullRequestCardPolicy = ({
policy,
}: PullRequestCardPolicyProps) => (
<PullRequestCardPolicyContainer>
{getPolicyIcon(policy)} <span>{policy.text}</span>
</PullRequestCardPolicyContainer>
);
@@ -0,0 +1,38 @@
/*
* 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 { Avatar } from '@backstage/core-components';
import React from 'react';
import { Reviewer } from '@backstage/plugin-azure-devops-common';
type PullRequestCardReviewerProps = {
reviewer: Reviewer;
};
export const PullRequestCardReviewer = ({
reviewer,
}: PullRequestCardReviewerProps) => (
<Avatar
displayName={reviewer.displayName}
picture={reviewer.imageUrl}
customStyles={{
width: '2.5rem',
height: '2.5rem',
fontSize: '1rem',
border: '0.3rem solid silver',
}}
/>
);
@@ -0,0 +1,42 @@
/*
* 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 { PullRequestCardReviewer } from './PullRequestCardReviewer';
import React from 'react';
import { Reviewer } from '@backstage/plugin-azure-devops-common';
import { reviewerFilter } from '../utils';
import { styled } from '@material-ui/core/styles';
const PullRequestCardReviewersContainer = styled('div')({
'& > *': {
marginTop: 4,
marginBottom: 4,
},
});
type PullRequestCardReviewersProps = {
reviewers: Reviewer[];
};
export const PullRequestCardReviewers = ({
reviewers,
}: PullRequestCardReviewersProps) => (
<PullRequestCardReviewersContainer>
{reviewers.filter(reviewerFilter).map(reviewer => (
<PullRequestCardReviewer key={reviewer.id} reviewer={reviewer} />
))}
</PullRequestCardReviewersContainer>
);
@@ -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 { PullRequestCard } from './PullRequestCard';
@@ -0,0 +1,51 @@
/*
* 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 { PullRequestGridColumn } from '../PullRequestGridColumn';
import { PullRequestGroup } from '../types';
import React from 'react';
import { styled } from '@material-ui/core';
const GridDiv = styled('div')(({ theme }) => ({
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
'& > *': {
marginRight: theme.spacing(2),
},
'& > :last-of-type': {
marginRight: 0,
},
}));
type PullRequestGridProps = {
pullRequestGroups: PullRequestGroup[];
};
export const PullRequestGrid = ({
pullRequestGroups,
}: PullRequestGridProps) => {
return (
<GridDiv>
{pullRequestGroups.map((pullRequestGroup, index) => (
<PullRequestGridColumn
key={index}
pullRequestGroup={pullRequestGroup}
/>
))}
</GridDiv>
);
};
@@ -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 { PullRequestGrid } from './PullRequestGrid';
@@ -0,0 +1,87 @@
/*
* 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 { Paper, Typography, styled, withStyles } from '@material-ui/core';
import { PullRequestCard } from '../PullRequestCard';
import { PullRequestGroup } from '../types';
import React from 'react';
const ColumnPaper = withStyles(theme => ({
root: {
display: 'flex',
flexDirection: 'column',
flex: 1,
padding: theme.spacing(2),
backgroundColor:
theme.palette.type === 'dark'
? theme.palette.grey[800]
: theme.palette.grey[300],
height: '100%',
},
}))(Paper);
const ColumnTitleDiv = styled('div')(({ theme }) => ({
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: theme.spacing(2),
}));
export const PullRequestCardContainer = styled('div')(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
'& > *': {
marginBottom: theme.spacing(2),
},
'& > :last-of-type': {
marginBottom: 0,
},
}));
type PullRequestGridColumnProps = {
pullRequestGroup: PullRequestGroup;
};
export const PullRequestGridColumn = ({
pullRequestGroup,
}: PullRequestGridColumnProps) => {
const columnTitle = (
<ColumnTitleDiv>
<Typography variant="h5">{pullRequestGroup.title}</Typography>
</ColumnTitleDiv>
);
const pullRequests = (
<PullRequestCardContainer>
{pullRequestGroup.pullRequests.map(pullRequest => (
<PullRequestCard
key={pullRequest.pullRequestId}
pullRequest={pullRequest}
simplified={pullRequestGroup.simplified}
/>
))}
</PullRequestCardContainer>
);
return (
<ColumnPaper>
{columnTitle}
{pullRequests}
</ColumnPaper>
);
};
@@ -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 { PullRequestGridColumn } from './PullRequestGridColumn';
@@ -0,0 +1,36 @@
/*
* 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,
Team,
} from '@backstage/plugin-azure-devops-common';
export interface PullRequestGroup {
title: string;
pullRequests: DashboardPullRequest[];
simplified?: boolean;
}
export type PullRequestFilter = (pullRequest: DashboardPullRequest) => boolean;
export type TeamFilter = (team: Team) => boolean;
export interface PullRequestGroupConfig {
title: string;
filter: PullRequestFilter;
simplified?: boolean;
}
@@ -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,
PullRequestVoteStatus,
Reviewer,
} from '@backstage/plugin-azure-devops-common';
import {
arrayExtract,
getCreatedByUserFilter,
getPullRequestGroups,
reviewerFilter,
} from './utils';
describe('getCreatedByUserFilter', () => {
it('should filter if pull request is created by user', () => {
const userEmail = 'user1@backstage.com';
const pr = {
createdBy: { uniqueName: userEmail },
} as DashboardPullRequest;
const result = getCreatedByUserFilter(userEmail)(pr);
expect(result).toBe(true);
});
it('should not filter if pull request is not created by user', () => {
const userEmail1 = 'user1@backstage.com';
const userEmail2 = 'user2@backstage.com';
const pr = {
createdBy: { uniqueName: userEmail1 },
} as DashboardPullRequest;
const result = getCreatedByUserFilter(userEmail2)(pr);
expect(result).toBe(false);
});
});
describe('reviewerFilter', () => {
it('should return false if reviewer has no vote and is not required', () => {
const reviewer = {
voteStatus: PullRequestVoteStatus.NoVote,
isRequired: false,
} as Reviewer;
const result = reviewerFilter(reviewer);
expect(result).toBe(false);
});
it('should return true if reviewer has no vote and is required', () => {
const reviewer = {
voteStatus: PullRequestVoteStatus.NoVote,
isRequired: true,
} as Reviewer;
const result = reviewerFilter(reviewer);
expect(result).toBe(true);
});
it('should return true if reviewer has a vote and is not a container', () => {
const reviewer = {
voteStatus: PullRequestVoteStatus.Approved,
isContainer: false,
} as Reviewer;
const result = reviewerFilter(reviewer);
expect(result).toBe(true);
});
it('should return true if reviewer has a vote and is a container', () => {
const reviewer = {
voteStatus: PullRequestVoteStatus.Approved,
isContainer: true,
} as Reviewer;
const result = reviewerFilter(reviewer);
expect(result).toBe(false);
});
});
describe('arrayExtract', () => {
it('should extract numbers greater than 3', () => {
const numbers = [1, 2, 3, 4, 5, 6];
const numberFilter = (num: number): boolean => num > 3;
const extractedNumbers = arrayExtract(numbers, numberFilter);
expect(numbers).toEqual([1, 2, 3]);
expect(extractedNumbers).toEqual([4, 5, 6]);
});
it('should extract even numbers', () => {
const numbers = [1, 2, 3, 4, 5, 6];
const numberFilter = (num: number): boolean => num % 2 === 0;
const extractedNumbers = arrayExtract(numbers, numberFilter);
expect(numbers).toEqual([1, 3, 5]);
expect(extractedNumbers).toEqual([2, 4, 6]);
});
});
describe('getPullRequestGroups', () => {
it('should create groups of pull requests based on the provided configs', () => {
const userEmail = 'user1@backstage.com';
const userEmail2 = 'user2@backstage.com';
const pullRequests = [
{
pullRequestId: 1,
createdBy: { uniqueName: userEmail },
} as DashboardPullRequest,
{
pullRequestId: 2,
createdBy: { uniqueName: userEmail },
} as DashboardPullRequest,
{
pullRequestId: 3,
createdBy: { uniqueName: userEmail2 },
} as DashboardPullRequest,
{
pullRequestId: 4,
createdBy: { uniqueName: userEmail2 },
} as DashboardPullRequest,
];
const configs = [
{ title: 'Created by me', filter: getCreatedByUserFilter(userEmail) },
{ title: 'Other PRs', filter: (_: unknown) => true, simplified: true },
];
const result = getPullRequestGroups(pullRequests, configs);
expect(result.length).toBe(2);
const group1 = result[0];
expect(group1.title).toBe('Created by me');
expect(group1.simplified).toBeFalsy();
expect(group1.pullRequests.length).toBe(2);
expect(group1.pullRequests).toContainEqual(
expect.objectContaining({ pullRequestId: 1 }),
);
expect(group1.pullRequests).toContainEqual(
expect.objectContaining({ pullRequestId: 2 }),
);
const group2 = result[1];
expect(group2.title).toBe('Other PRs');
expect(group2.simplified).toBe(true);
expect(group2.pullRequests.length).toBe(2);
expect(group2.pullRequests).toContainEqual(
expect.objectContaining({ pullRequestId: 3 }),
);
expect(group2.pullRequests).toContainEqual(
expect.objectContaining({ pullRequestId: 4 }),
);
});
});
@@ -0,0 +1,117 @@
/*
* 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,
PullRequestVoteStatus,
Reviewer,
} from '@backstage/plugin-azure-devops-common';
import {
PullRequestFilter,
PullRequestGroup,
PullRequestGroupConfig,
} from './types';
/**
* Creates a filter that matches pull requests created by `userEmail`.
* @param userEmail an email to filter by.
* @returns a filter for pull requests created by `userEmail`.
*/
export function getCreatedByUserFilter(
userEmail: string | undefined,
): PullRequestFilter {
return (pullRequest: DashboardPullRequest): boolean =>
pullRequest.createdBy?.uniqueName?.toLocaleLowerCase() ===
userEmail?.toLocaleLowerCase();
}
/**
* Filters a reviewer based on vote status and if the reviewer is required.
* @param reviewer a reviewer to filter.
* @returns whether or not to filter the `reviewer`.
*/
export function reviewerFilter(reviewer: Reviewer): boolean {
return reviewer.voteStatus === PullRequestVoteStatus.NoVote
? !!reviewer.isRequired
: !reviewer.isContainer;
}
/**
* Removes values from the provided array and returns them.
* @param arr the array to extract values from.
* @param filter a filter used to extract values from the provided array.
* @returns the values that were extracted from the array.
*
* @example
* ```ts
* const numbers = [1, 2, 3, 4, 5, 6];
* const numberFilter = (num: number): boolean => num > 3;
* const extractedNumbers = arrayExtract(numbers, numberFilter);
* console.log(numbers); // [1, 2, 3]
* console.log(extractedNumbers); // [4, 5, 6]
* ```
*
* @example
* ```ts
* const numbers = [1, 2, 3, 4, 5, 6];
* const numberFilter = (num: number): boolean => num % 2 === 0;
* const extractedNumbers = arrayExtract(numbers, numberFilter);
* console.log(numbers); // [1, 3, 5]
* console.log(extractedNumbers); // [2, 4, 6]
* ```
*/
export function arrayExtract<T>(arr: T[], filter: (value: T) => unknown): T[] {
const extractedValues: T[] = [];
for (let i = 0; i - extractedValues.length < arr.length; i++) {
const offsetIndex = i - extractedValues.length;
const value = arr[offsetIndex];
if (filter(value)) {
arr.splice(offsetIndex, 1);
extractedValues.push(value);
}
}
return extractedValues;
}
/**
* Creates groups of pull requests based on a list of `PullRequestGroupConfig`.
* @param pullRequests all pull requests to be split up into groups.
* @param configs the config used for splitting up the pull request groups.
* @returns a list of pull request groups.
*/
export function getPullRequestGroups(
pullRequests: DashboardPullRequest[],
configs: PullRequestGroupConfig[],
): PullRequestGroup[] {
const remainingPullRequests: DashboardPullRequest[] = [...pullRequests];
const pullRequestGroups: PullRequestGroup[] = [];
configs.forEach(({ title, filter: configFilter, simplified }) => {
const groupPullRequests = arrayExtract(remainingPullRequests, configFilter);
pullRequestGroups.push({
title,
pullRequests: groupPullRequests,
simplified,
});
});
return pullRequestGroups;
}
+22
View File
@@ -0,0 +1,22 @@
/*
* 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 './useAllTeams';
export * from './useDashboardPullRequests';
export * from './useProjectRepoFromEntity';
export * from './usePullRequests';
export * from './useRepoBuilds';
export * from './useUserEmail';
@@ -0,0 +1,42 @@
/*
* 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 { Team } from '@backstage/plugin-azure-devops-common';
import { azureDevOpsApiRef } from '../api';
import { useApi } from '@backstage/core-plugin-api';
import { useAsync } from 'react-use';
export function useAllTeams(): {
teams?: Team[];
loading: boolean;
error?: Error;
} {
const api = useApi(azureDevOpsApiRef);
const {
value: teams,
loading,
error,
} = useAsync(() => {
return api.getAllTeams();
}, [api]);
return {
teams,
loading,
error,
};
}
@@ -0,0 +1,42 @@
/*
* 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 } from '@backstage/plugin-azure-devops-common';
import { azureDevOpsApiRef } from '../api';
import { useApi } from '@backstage/core-plugin-api';
import { useAsync } from 'react-use';
export function useDashboardPullRequests(project: string): {
pullRequests?: DashboardPullRequest[];
loading: boolean;
error?: Error;
} {
const api = useApi(azureDevOpsApiRef);
const {
value: pullRequests,
loading,
error,
} = useAsync(() => {
return api.getDashboardPullRequests(project);
}, [api, project]);
return {
pullRequests,
loading,
error,
};
}
@@ -0,0 +1,22 @@
/*
* 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 { identityApiRef, useApi } from '@backstage/core-plugin-api';
export function useUserEmail(): string | undefined {
const identityApi = useApi(identityApiRef);
return identityApi.getProfile().email;
}
+4
View File
@@ -13,9 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export {
azureDevOpsPlugin,
EntityAzurePipelinesContent,
EntityAzurePullRequestsContent,
isAzureDevOpsAvailable,
AzurePullRequestsPage,
} from './plugin';
export { AzurePullRequestsIcon } from './components/AzurePullRequestsIcon';
+10
View File
@@ -16,6 +16,7 @@
import {
azurePipelinesEntityContentRouteRef,
azurePullRequestDashboardRouteRef,
azurePullRequestsEntityContentRouteRef,
} from './routes';
import {
@@ -46,6 +47,15 @@ export const azureDevOpsPlugin = createPlugin({
],
});
export const AzurePullRequestsPage = azureDevOpsPlugin.provide(
createRoutableExtension({
name: 'AzurePullRequestsPage',
component: () =>
import('./components/PullRequestsPage').then(m => m.PullRequestsPage),
mountPoint: azurePullRequestDashboardRouteRef,
}),
);
export const EntityAzurePipelinesContent = azureDevOpsPlugin.provide(
createRoutableExtension({
name: 'EntityAzurePipelinesContent',
+5
View File
@@ -16,6 +16,11 @@
import { createRouteRef } from '@backstage/core-plugin-api';
export const azurePullRequestDashboardRouteRef = createRouteRef({
id: 'azure-pull-request-dashboard',
path: '',
});
export const azurePipelinesEntityContentRouteRef = createRouteRef({
id: 'azure-pipelines-entity-content',
});