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:
@@ -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.
|
||||
@@ -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 />
|
||||
|
||||
@@ -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)
|
||||
```
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
+32
@@ -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';
|
||||
+118
@@ -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>
|
||||
);
|
||||
+130
@@ -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>
|
||||
);
|
||||
};
|
||||
+35
@@ -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>
|
||||
);
|
||||
+95
@@ -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>
|
||||
);
|
||||
+38
@@ -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',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
+42
@@ -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';
|
||||
+51
@@ -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';
|
||||
+87
@@ -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>
|
||||
);
|
||||
};
|
||||
+17
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user