Added entity pull requests

Signed-off-by: Andre Wanlin <awanlin@rapidrtc.com>
This commit is contained in:
Andre Wanlin
2021-10-31 12:18:33 -05:00
parent 056c85aa72
commit adcb1fa7a2
26 changed files with 654 additions and 10 deletions
@@ -15,6 +15,8 @@
*/
import {
PullRequest,
PullRequestOptions,
RepoBuild,
RepoBuildOptions,
} from '@backstage/plugin-azure-devops-common';
@@ -33,4 +35,10 @@ export interface AzureDevOpsApi {
repoName: string,
options?: RepoBuildOptions,
): Promise<{ items: RepoBuild[] }>;
getPullRequests(
projectName: string,
repoName: string,
options?: PullRequestOptions,
): Promise<{ items: PullRequest[] }>;
}
@@ -16,6 +16,8 @@
import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api';
import {
PullRequest,
PullRequestOptions,
RepoBuild,
RepoBuildOptions,
} from '@backstage/plugin-azure-devops-common';
@@ -46,6 +48,17 @@ export class AzureDevOpsClient implements AzureDevOpsApi {
return { items };
}
async getPullRequests(
projectName: string,
repoName: string,
options?: PullRequestOptions,
): Promise<{ items: PullRequest[] }> {
const items = await this.get<PullRequest[]>(
`pull-requests/${projectName}/${repoName}?top=${options?.top}&status=${options?.status}`,
);
return { items };
}
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,54 @@
/*
* 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 { makeStyles, useTheme } from '@material-ui/core';
import AzurePipelinesSvg from '../../icons/alt-azure-pipelines-icon.svg';
import React from 'react';
const useStyles = makeStyles({
darkMode: {
filter: 'invert(100%)',
},
lightMode: {
filter: 'invert(0%)',
},
});
export const AzurePipelinesIcon = ({
width,
height,
}: {
width: string;
height: string;
}) => {
const theme = useTheme();
const classes = useStyles();
return (
<img
src={AzurePipelinesSvg}
alt="Azure Pipelines"
width={width}
height={height}
className={
theme.palette.type.toString() === 'dark'
? classes.darkMode
: classes.lightMode
}
/>
);
};
@@ -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 { AzurePipelinesIcon } from './AzurePipelinesIcon';
@@ -0,0 +1,54 @@
/*
* 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 { makeStyles, useTheme } from '@material-ui/core';
import AzureReposSvg from '../../icons/alt-azure-repos-icon.svg';
import React from 'react';
const useStyles = makeStyles({
darkMode: {
filter: 'invert(100%)',
},
lightMode: {
filter: 'invert(0%)',
},
});
export const AzureReposIcon = ({
width,
height,
}: {
width: string;
height: string;
}) => {
const theme = useTheme();
const classes = useStyles();
return (
<img
src={AzureReposSvg}
alt="Azure Repos"
width={width}
height={height}
className={
theme.palette.type.toString() === 'dark'
? classes.darkMode
: classes.lightMode
}
/>
);
};
@@ -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 { AzureReposIcon } from './AzureReposIcon';
@@ -18,7 +18,10 @@ import {
BuildResult,
BuildStatus,
} from '@backstage/plugin-azure-devops-common';
import { getBuildResultComponent, getBuildStateComponent } from './BuildTable';
import {
getBuildResultComponent,
getBuildStateComponent
} from './BuildTable';
import { renderInTestApp } from '@backstage/test-utils';
@@ -238,4 +241,4 @@ describe('getBuildStateComponent', () => {
expect(getByText('Unknown')).toBeInTheDocument();
});
});
});
});
@@ -20,6 +20,7 @@ import {
BuildStatus,
RepoBuild,
} from '@backstage/plugin-azure-devops-common';
import { DateTime, Interval } from 'luxon'
import {
Link,
ResponseErrorPanel,
@@ -33,8 +34,9 @@ import {
TableColumn,
} from '@backstage/core-components';
import { DateTime } from 'luxon';
import { AzurePipelinesIcon } from '../AzurePipelinesIcon';
import React from 'react';
import humanizeDuration from 'humanize-duration';
export const getBuildResultComponent = (result: number | undefined) => {
switch (result) {
@@ -113,6 +115,43 @@ export const getBuildStateComponent = (
}
};
export const getBuildDuration = (startTime?: Date, finishTime?: Date) => {
if (!startTime || (!startTime && !finishTime)) {
return '';
}
const start = DateTime.fromISO(startTime.toString());
const finish = finishTime
? DateTime.fromISO(finishTime.toString())
: DateTime.now();
const formatted = Interval.fromDateTimes(start, finish)
.toDuration()
.valueOf();
const shortEnglishHumanizer = humanizeDuration.humanizer({
language: 'shortEn',
languages: {
shortEn: {
y: () => 'y',
mo: () => 'mo',
w: () => 'w',
d: () => 'd',
h: () => 'h',
m: () => 'm',
s: () => 's',
ms: () => 'ms',
},
},
});
return shortEnglishHumanizer(formatted, {
largest: 2,
round: true,
spacer: '',
});
};
const columns: TableColumn[] = [
{
title: 'ID',
@@ -144,6 +183,18 @@ const columns: TableColumn[] = [
</Box>
),
},
{
title: 'Duration',
field: 'queueTime',
width: 'auto',
render: (row: Partial<RepoBuild>) => (
<Box display="flex" alignItems="center">
<Typography>
{getBuildDuration(row.startTime, row.finishTime)}
</Typography>
</Box>
),
},
{
title: 'Age',
field: 'queueTime',
@@ -178,7 +229,13 @@ export const BuildTable = ({ items, loading, error }: BuildTableProps) => {
pageSize: 5,
showEmptyDataSourceMessage: !loading,
}}
title={`Builds (${items ? items.length : 0})`}
title={
<Box display="flex" alignItems="center">
<AzurePipelinesIcon width="30px" height="30px" />
<Box mr={1} />
Azure Pipelines - Builds ({items ? items.length : 0})
</Box>
}
data={items ?? []}
/>
);
@@ -0,0 +1,26 @@
/*
* 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 { PullRequestTable } from '../PullRequestTable/PullRequestTable';
import React from 'react';
export const EntityPageAzureRepos = ({
defaultLimit,
}: {
defaultLimit?: number;
}) => {
return <PullRequestTable defaultLimit={defaultLimit} />;
};
@@ -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 { EntityPageAzureRepos } from './EntityPageAzureRepos';
@@ -0,0 +1,69 @@
/*
* 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 { Button, ButtonGroup } from '@material-ui/core';
import { PullRequestStatus } from 'azure-devops-node-api/interfaces/GitInterfaces';
import React from 'react';
export const PullRequestStatusButtonGroup = ({
pullRequestStatusState,
setPullRequestsStatusState,
}: {
pullRequestStatusState: PullRequestStatus;
setPullRequestsStatusState: any;
}) => {
return (
<ButtonGroup aria-label="outlined button group">
<Button
color={
pullRequestStatusState === PullRequestStatus.Active
? 'primary'
: 'default'
}
onClick={() => {
setPullRequestsStatusState(PullRequestStatus.Active);
}}
>
Active
</Button>
<Button
color={
pullRequestStatusState === PullRequestStatus.Completed
? 'primary'
: 'default'
}
onClick={() => {
setPullRequestsStatusState(PullRequestStatus.Completed);
}}
>
Completed
</Button>
<Button
color={
pullRequestStatusState === PullRequestStatus.Abandoned
? 'primary'
: 'default'
}
onClick={() => {
setPullRequestsStatusState(PullRequestStatus.Abandoned);
}}
>
Abandoned
</Button>
</ButtonGroup>
);
};
@@ -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 { PullRequestStatusButtonGroup } from './PullRequestStatusButtonGroup';
@@ -0,0 +1,136 @@
/*
* 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 { Box, Chip } from '@material-ui/core';
import {
Link,
ResponseErrorPanel,
Table,
TableColumn,
} from '@backstage/core-components';
import React, { useState } from 'react';
import { AzureReposIcon } from '../AzureReposIcon';
import { DateTime } from 'luxon';
import { PullRequest } from '../../api/types';
import { PullRequestStatus } from 'azure-devops-node-api/interfaces/GitInterfaces';
import { PullRequestStatusButtonGroup } from '../PullRequestStatusButtonGroup';
import { useEntity } from '@backstage/plugin-catalog-react';
import { usePullRequests } from '../../hooks/usePullRequests';
const columns: TableColumn[] = [
{
title: 'ID',
field: 'pullRequestId',
highlight: false,
width: 'auto',
},
{
title: 'Title',
field: 'title',
width: 'auto',
render: (row: Partial<PullRequest>) => (
<Box display="flex" alignItems="center">
<Link to={row.link || ''}>{row.title}</Link>
<Box paddingLeft={1}>
{row.isDraft && (
<Chip
label="Draft"
variant="outlined"
color="secondary"
size="small"
/>
)}
</Box>
</Box>
),
},
{
title: 'Source',
field: 'sourceRefName',
width: 'auto',
},
{
title: 'Target',
field: 'targetRefName',
width: 'auto',
},
{
title: 'Created By',
field: 'createdBy',
width: 'auto',
},
{
title: 'Created',
field: 'creationDate',
width: 'auto',
render: (row: Partial<PullRequest>) =>
DateTime.fromISO(
row.creationDate ? row.creationDate.toString() : new Date().toString(),
).toRelative(),
},
];
type PullRequestTableProps = {
defaultLimit?: number;
};
export const PullRequestTable = ({ defaultLimit }: PullRequestTableProps) => {
const [pullRequestStatusState, setPullRequestStatusState] =
useState<PullRequestStatus>(PullRequestStatus.Active);
const { entity } = useEntity();
const { items, loading, error } = usePullRequests(
entity,
defaultLimit,
pullRequestStatusState,
);
if (error) {
return (
<div>
<ResponseErrorPanel error={error} />
</div>
);
}
return (
<Table
isLoading={loading}
columns={columns}
options={{
search: true,
paging: true,
pageSize: 5,
showEmptyDataSourceMessage: !loading,
}}
title={
<Box display="flex" alignItems="center">
<AzureReposIcon width="30px" height="30px" />
<Box mr={1} />
Azure Repos - Pull Requests ({items ? items.length : 0})
<Box position="absolute" right={320} top={20}>
<PullRequestStatusButtonGroup
pullRequestStatusState={pullRequestStatusState}
setPullRequestsStatusState={setPullRequestStatusState}
/>
</Box>
</Box>
}
data={items ?? []}
/>
);
};
@@ -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 { PullRequestTable } from './PullRequestTable';
@@ -0,0 +1,55 @@
/*
* 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 { PullRequest, PullRequestOptions } from '../api/types';
import { AZURE_DEVOPS_DEFAULT_TOP } from '../constants';
import { Entity } from '@backstage/catalog-model';
import { PullRequestStatus } from 'azure-devops-node-api/interfaces/GitInterfaces';
import { azureDevOpsApiRef } from '../api';
import { useApi } from '@backstage/core-plugin-api';
import { useAsync } from 'react-use';
import { useProjectRepoFromEntity } from './useProjectRepoFromEntity';
export function usePullRequests(
entity: Entity,
defaultLimit?: number,
requestedStatus?: PullRequestStatus,
): {
items?: PullRequest[];
loading: boolean;
error?: Error;
} {
const top = defaultLimit ?? AZURE_DEVOPS_DEFAULT_TOP;
const status = requestedStatus ?? PullRequestStatus.Active;
const options: PullRequestOptions = {
top: top,
status: status,
};
const api = useApi(azureDevOpsApiRef);
const { project, repo } = useProjectRepoFromEntity(entity);
const { value, loading, error } = useAsync(() => {
return api.getPullRequests(project, repo, options);
}, [api, project, repo, entity, requestedStatus]);
return {
items: value?.items,
loading,
error,
};
}
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512"><title>Rocket</title><path d="M461.81 53.81a4.4 4.4 0 00-3.3-3.39c-54.38-13.3-180 34.09-248.13 102.17a294.9 294.9 0 00-33.09 39.08c-21-1.9-42-.3-59.88 7.5-50.49 22.2-65.18 80.18-69.28 105.07a9 9 0 009.8 10.4l81.07-8.9a180.29 180.29 0 001.1 18.3 18.15 18.15 0 005.3 11.09l31.39 31.39a18.15 18.15 0 0011.1 5.3 179.91 179.91 0 0018.19 1.1l-8.89 81a9 9 0 0010.39 9.79c24.9-4 83-18.69 105.07-69.17 7.8-17.9 9.4-38.79 7.6-59.69a293.91 293.91 0 0039.19-33.09c68.38-68 115.47-190.86 102.37-247.95zM298.66 213.67a42.7 42.7 0 1160.38 0 42.65 42.65 0 01-60.38 0z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/><path d="M109.64 352a45.06 45.06 0 00-26.35 12.84C65.67 382.52 64 448 64 448s65.52-1.67 83.15-19.31A44.73 44.73 0 00160 402.32" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/></svg>

After

Width:  |  Height:  |  Size: 965 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512"><title>Git Pull Request</title><circle cx="128" cy="416" r="48" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M128 144v224M288 160l-64-64 64-64"/><circle cx="128" cy="96" r="48" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/><circle cx="384" cy="416" r="48" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/><path d="M240 96h84a60 60 0 0160 60v212" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/></svg>

After

Width:  |  Height:  |  Size: 798 B

+1
View File
@@ -16,5 +16,6 @@
export {
azureDevOpsPlugin,
EntityAzurePipelinesContent,
EntityAzureReposContent,
isAzureDevOpsAvailable,
} from './plugin';
+11
View File
@@ -56,3 +56,14 @@ export const EntityAzurePipelinesContent = azureDevOpsPlugin.provide(
mountPoint: azureDevOpsRouteRef,
}),
);
export const EntityAzureReposContent = azureDevOpsPlugin.provide(
createRoutableExtension({
name: 'EntityAzureReposContent',
component: () =>
import('./components/EntityPageAzureRepos').then(
m => m.EntityPageAzureRepos,
),
mountPoint: azureDevOpsRouteRef,
}),
);