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
+50 -3
View File
@@ -6,10 +6,16 @@ Website: [https://dev.azure.com/](https://dev.azure.com/)
### Azure Pipelines
Lists the top _n_ builds for a given repository where _n_ is a configurable value
Lists the top _n_ builds for a given Azure Repo where _n_ is a configurable value
![Azure Pipelines Builds Example](./docs/azure-devops-builds.png)
### Azure Repos
Lists the top _n_ Active, Completed, or Abandoned Pull Requests for a given repository where _n_ is a configurable value
![Azure Repos Pull Requests Example](./docs/azure-devops-pull-requests.png)
## Setup
The following sections will help you get the Azure DevOps plugin setup and running
@@ -69,14 +75,55 @@ To get the Azure Pipelines component working you'll need to do the following two
<EntitySwitch>
// ...
<EntitySwitch.Case if={isAzureDevOpsAvailable}>
// Set defaultLimit to the max number of builds you would like to be able to see
// the default if not set is 10
<EntityAzurePipelinesContent defaultLimit={25} />
</EntitySwitch.Case>
// ...
</EntitySwitch>
```
**Notes:**
- The `if` prop is optional on the `EntitySwitch.Case`, you can remove it if you always want to see the tab even if the entity being viewed does not have the needed annotation
- The `defaultLimit` proper on the `EntityAzurePipelinesContent` will set the max number of Builds you would like to see, if not set this will default to 10
### Azure Repos Component
To get the Azure Repos component working you'll need to do the following two steps:
1. First we need to add the @backstage/plugin-azure-devops package to your frontend app:
```bash
# From your Backstage root directory
cd packages/app
yarn add @backstage/plugin-azure-devops
```
2. Second we need to add the `EntityAzureReposContent` extension to the entity page in your app:
```tsx
// In packages/app/src/components/catalog/EntityPage.tsx
import {
EntityAzureReposContent,
isAzureDevOpsAvailable,
} from '@backstage/plugin-azure-devops';
// For example in the Service section
const serviceEntityPage = (
<EntityLayout>
// ...
<EntityLayout.Route if={isAzureDevOpsAvailable} path="/pull-requests" title="Pull Requests">
<EntityAzureReposContent defaultLimit={25} />
</EntityLayout.Route>
// ...
</EntityLayout>
```
**Notes:**
- You'll need to add the `EntityLayout.Route` above from step 2 to all the entity sections you want to see Pull Requests in. For example if you wanted to see Pull Requests when looking at Website entities then you would need to add this to the `websiteEntityPage` section.
- The `if` prop is optional on the `EntityLayout.Route`, you can remove it if you always want to see the tab even if the entity being viewed does not have the needed annotation
- The `defaultLimit` proper on the `EntityAzureReposContent` will set the max number of Pull Requests you would like to see, if not set this will default to 10
## Limitations
- Currently multiple organizations are not supported
+9
View File
@@ -28,6 +28,15 @@ export const EntityAzurePipelinesContent: ({
defaultLimit?: number | undefined;
}) => JSX.Element;
// Warning: (ae-missing-release-tag) "EntityAzureReposContent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const EntityAzureReposContent: ({
defaultLimit,
}: {
defaultLimit?: number | undefined;
}) => JSX.Element;
// Warning: (ae-missing-release-tag) "isAzureDevOpsAvailable" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 173 KiB

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

+1
View File
@@ -37,6 +37,7 @@
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.57",
"humanize-duration": "^3.27.0",
"luxon": "^2.0.2",
"react": "^16.13.1",
"react-dom": "^16.13.1",
@@ -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,
}),
);