Made changes requested in the pull request

Signed-off-by: Alisson Fabiano <afabiano@eshopworld.com>
This commit is contained in:
Alisson Fabiano
2022-08-19 10:50:59 +01:00
parent 0cec11b6e8
commit 889aed210f
14 changed files with 213 additions and 172 deletions
+2 -1
View File
@@ -32,7 +32,8 @@
"express-promise-router": "^4.1.0",
"p-limit": "^3.1.0",
"winston": "^3.2.1",
"yn": "^4.0.0"
"yn": "^4.0.0",
"mime-types": "^2.1.27"
},
"devDependencies": {
"@backstage/cli": "^0.18.1-next.0",
@@ -43,6 +43,7 @@ import {
convertPolicy,
getArtifactId,
replaceReadme,
buildEncodedUrl,
} from '../utils';
import { TeamMember as AdoTeamMember } from 'azure-devops-node-api/interfaces/common/VSSInterfaces';
@@ -399,32 +400,24 @@ export class AzureDevOpsApi {
public async getReadme(
host: string,
organization: string,
projectName: string,
repoName: string,
org: string,
project: string,
repo: string,
): Promise<{
url: string;
content: string;
}> {
const getFileContent = async (
path: string,
encoding?: BufferEncoding,
): Promise<{
url: string;
content: string;
}> => {
const url = `https://${host}/${organization}/${projectName}/_git/${repoName}?path=${path}`;
const response = await this.urlReader.read(url);
return {
url,
content: Buffer.from(response).toString(encoding),
};
};
const { url, content } = await getFileContent('README.md');
return {
url,
content: await replaceReadme(content, getFileContent),
};
const url = buildEncodedUrl(host, org, project, repo, 'README.md');
const response = await this.urlReader.read(url);
const content = await replaceReadme(
this.urlReader,
host,
org,
project,
repo,
response.toString(),
);
return { url, content };
}
}
@@ -33,7 +33,7 @@ import { ConfigReader } from '@backstage/config';
import { GitRepository } from 'azure-devops-node-api/interfaces/GitInterfaces';
import { createRouter } from './router';
import express from 'express';
import { getVoidLogger } from '@backstage/backend-common';
import { getVoidLogger, UrlReaders } from '@backstage/backend-common';
import request from 'supertest';
describe('createRouter', () => {
@@ -55,18 +55,28 @@ describe('createRouter', () => {
getTeamMembers: jest.fn(),
getReadme: jest.fn(),
} as any;
const config = new ConfigReader({
azureDevOps: {
token: 'foo',
host: 'host.com',
organization: 'myOrg',
top: 5,
},
});
const logger = getVoidLogger();
const router = await createRouter({
config,
logger,
azureDevOpsApi,
logger: getVoidLogger(),
config: new ConfigReader({
azureDevOps: {
token: 'foo',
host: 'host.com',
organization: 'myOrg',
top: 5,
},
reader: UrlReaders.default({
config,
logger,
}),
});
app = express().use(router);
});
@@ -26,7 +26,7 @@ import { Config } from '@backstage/config';
import { Logger } from 'winston';
import { PullRequestsDashboardProvider } from '../api/PullRequestsDashboardProvider';
import Router from 'express-promise-router';
import { errorHandler, UrlReaders } from '@backstage/backend-common';
import { errorHandler, UrlReader } from '@backstage/backend-common';
import express from 'express';
const DEFAULT_TOP = 10;
@@ -35,18 +35,18 @@ export interface RouterOptions {
azureDevOpsApi?: AzureDevOpsApi;
logger: Logger;
config: Config;
reader: UrlReader;
}
export async function createRouter(
options: RouterOptions,
): Promise<express.Router> {
const { logger } = options;
const { logger, reader } = options;
const config = options.config.getConfig('azureDevOps');
const token = config.getString('token');
const host = config.getString('host');
const organization = config.getString('organization');
const reader = UrlReaders.default(options);
const authHandler = getPersonalAccessTokenHandler(token);
const webApi = new WebApi(`https://${host}/${organization}`, authHandler);
@@ -17,6 +17,7 @@
import {
createServiceBuilder,
loadBackendConfig,
UrlReaders,
} from '@backstage/backend-common';
import { Server } from 'http';
import { Logger } from 'winston';
@@ -39,6 +40,7 @@ export async function startStandaloneServer(
const router = await createRouter({
logger,
config,
reader: UrlReaders.default({ logger, config }),
});
let service = createServiceBuilder(module)
@@ -25,12 +25,11 @@ import {
extractPartsFromAsset,
getArtifactId,
getAvatarUrl,
getMimeByExtension,
getPullRequestLink,
replaceReadme,
} from './azure-devops-utils';
import { GitPullRequest } from 'azure-devops-node-api/interfaces/GitInterfaces';
import { UrlReader } from '@backstage/backend-common';
describe('convertDashboardPullRequest', () => {
it('should return DashboardPullRequest', () => {
@@ -236,16 +235,6 @@ describe('extractPartsFromAsset', () => {
});
});
describe('getMimeByExtension', () => {
it('should return mime type', () => {
expect(getMimeByExtension('.png')).toBe('image/png');
expect(getMimeByExtension('.jpg')).toBe('image/jpeg');
expect(getMimeByExtension('.jpeg')).toBe('image/jpeg');
expect(getMimeByExtension('.webp')).toBe('image/webp');
expect(getMimeByExtension('.gif')).toBe('image/gif');
});
});
describe('replaceReadme', () => {
it('should return mime type', async () => {
const readme = `
@@ -257,31 +246,29 @@ describe('replaceReadme', () => {
![Image 5](/images/sa)mple.gif)
`;
async function mockFileContent(
path: string,
encoding?: BufferEncoding,
): Promise<{
url: string;
content: string;
}> {
expect(encoding).toBe('base64');
return new Promise(resolve =>
resolve({
url: '',
content: Buffer.from(path).toString(encoding),
}),
);
}
const reader: UrlReader = {
read: url => new Promise<Buffer>(resolve => resolve(Buffer.from(url))),
readTree: jest.fn(),
search: jest.fn(),
readUrl: jest.fn(),
};
const result = await replaceReadme(readme, mockFileContent);
const result = await replaceReadme(
reader,
'host',
'org',
'project',
'repo',
readme,
);
const expected = `
## Images
![Image 1](data:image/png;base64,L2ltYWdlcy9zYW1wbGUtNCgyKS5wbmc=)
![Image 2](data:image/jpeg;base64,L2ltYWdlcy9jZENTaistMDEyMzQwLmpwZw==)
![Image 3](data:image/jpeg;base64,L2ltYWdlcy90ZXN0LTQoMikpKSkuanBlZw==)
![Image 4](data:image/webp;base64,L2ltYWdlcy90ZXN0LTIyMTFqZC53ZWJw)
![Image 5](data:image/gif;base64,L2ltYWdlcy9zYSltcGxlLmdpZg==)
![Image 1](data:image/png;base64,aHR0cHM6Ly9ob3N0L29yZy9wcm9qZWN0L19naXQvcmVwbz9wYXRoPSUyRmltYWdlcyUyRnNhbXBsZS00KDIpLnBuZw==)
![Image 2](data:image/jpeg;base64,aHR0cHM6Ly9ob3N0L29yZy9wcm9qZWN0L19naXQvcmVwbz9wYXRoPSUyRmltYWdlcyUyRmNkQ1NqJTJCLTAxMjM0MC5qcGc=)
![Image 3](data:image/jpeg;base64,aHR0cHM6Ly9ob3N0L29yZy9wcm9qZWN0L19naXQvcmVwbz9wYXRoPSUyRmltYWdlcyUyRnRlc3QtNCgyKSkpKS5qcGVn)
![Image 4](data:image/webp;base64,aHR0cHM6Ly9ob3N0L29yZy9wcm9qZWN0L19naXQvcmVwbz9wYXRoPSUyRmltYWdlcyUyRnRlc3QtMjIxMWpkLndlYnA=)
![Image 5](data:image/gif;base64,aHR0cHM6Ly9ob3N0L29yZy9wcm9qZWN0L19naXQvcmVwbz9wYXRoPSUyRmltYWdlcyUyRnNhKW1wbGUuZ2lm)
`;
expect(expected).toBe(result);
@@ -30,9 +30,11 @@ import {
GitRepository,
IdentityRefWithVote,
} from 'azure-devops-node-api/interfaces/GitInterfaces';
import mime from 'mime-types';
import { IdentityRef } from 'azure-devops-node-api/interfaces/common/VSSInterfaces';
import { PolicyEvaluationRecord } from 'azure-devops-node-api/interfaces/PolicyInterfaces';
import { UrlReader } from '@backstage/backend-common';
export function convertDashboardPullRequest(
pullRequest: GitPullRequest,
@@ -206,31 +208,47 @@ export function convertPolicy(
}
export async function replaceReadme(
readme: string,
getFileContent: (
path: string,
encoding?: BufferEncoding,
) => Promise<{
url: string;
content: string;
}>,
urlReader: UrlReader,
host: string,
org: string,
project: string,
repo: string,
readmeContent: string,
) {
const filesPath = extractAssets(readme);
const filesPath = extractAssets(readmeContent);
if (!filesPath) return readmeContent;
return await filesPath.reduce(
async (promise: Promise<string>, filePath: string) =>
promise.then(async content => {
const { label, path, ext } = extractPartsFromAsset(filePath);
const mime = getMimeByExtension(ext);
const file = await getFileContent(path + ext, 'base64');
const data = mime.lookup(ext);
const url = buildEncodedUrl(host, org, project, repo, path + ext);
const buffer = await urlReader.read(url);
const file = await buffer.toString('base64');
return content.replace(
filePath,
`[${label}](data:${mime};base64,${file.content})`,
`[${label}](data:${data};base64,${file})`,
);
}),
Promise.resolve(readme),
Promise.resolve(readmeContent),
);
}
export function buildEncodedUrl(
host: string,
org: string,
project: string,
repo: string,
path: string,
): string {
const encodedHost = encodeURIComponent(host);
const encodedOrg = encodeURIComponent(org);
const encodedProject = encodeURIComponent(project);
const encodedRepo = encodeURIComponent(repo);
const encodedPath = encodeURIComponent(path);
return `https://${encodedHost}/${encodedOrg}/${encodedProject}/_git/${encodedRepo}?path=${encodedPath}`;
}
function convertReviewer(
identityRef?: IdentityRefWithVote,
): Reviewer | undefined {
@@ -293,7 +311,7 @@ function hasAutoComplete(pullRequest: GitPullRequest): boolean {
export function extractAssets(content: string) {
const regExp =
/\[([^\[\]]*)\]\((?!https?:\/\/)(.*?)(\.png|\.jpg|\.jpeg|\.gif|\.webp)(.*)\)/gim;
return content.match(regExp) || [];
return content.match(regExp);
}
export function extractPartsFromAsset(content: string): {
@@ -310,15 +328,3 @@ export function extractPartsFromAsset(content: string): {
path: path.startsWith('.') ? path.substring(1, path.length) : path,
};
}
export function getMimeByExtension(ext: string): string {
return (
{
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.webp': 'image/webp',
}[ext] || ''
);
}
@@ -67,5 +67,5 @@ export interface AzureDevOpsApi {
options?: BuildRunOptions,
): Promise<{ items: BuildRun[] }>;
getReadme(opts: ReadmeConfig): Promise<Readme | undefined>;
getReadme(opts: ReadmeConfig): Promise<Readme>;
}
@@ -32,10 +32,6 @@ import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api';
import { AzureDevOpsApi } from './AzureDevOpsApi';
import { ResponseError } from '@backstage/errors';
function isNotFoundError(error: any): error is ResponseError {
return error.response.status === 404;
}
export class AzureDevOpsClient implements AzureDevOpsApi {
private readonly discoveryApi: DiscoveryApi;
private readonly identityApi: IdentityApi;
@@ -136,22 +132,13 @@ export class AzureDevOpsClient implements AzureDevOpsApi {
return { items };
}
public getReadme = async (
opts: ReadmeConfig,
): Promise<Readme | undefined> => {
try {
return await this.get(
`readme/${encodeURIComponent(opts.project)}/${encodeURIComponent(
opts.repo,
)}`,
);
} catch (e) {
if (isNotFoundError(e)) {
return undefined;
}
return e;
}
};
public async getReadme(opts: ReadmeConfig): Promise<Readme> {
return await this.get(
`readme/${encodeURIComponent(opts.project)}/${encodeURIComponent(
opts.repo,
)}`,
);
}
private async get<T>(path: string): Promise<T> {
const baseUrl = `${await this.discoveryApi.getBaseUrl('azure-devops')}/`;
@@ -14,13 +14,13 @@
* limitations under the License.
*/
import { Button, makeStyles } from '@material-ui/core';
import { Alert } from '@material-ui/lab';
import { Box, Button, makeStyles } from '@material-ui/core';
import {
InfoCard,
Progress,
MarkdownContent,
EmptyState,
ErrorPanel,
} from '@backstage/core-components';
import { useEntity } from '@backstage/plugin-catalog-react';
import { useProjectRepoFromEntity } from '../../hooks';
@@ -30,15 +30,6 @@ import { azureDevOpsApiRef } from '../../api';
import useAsync from 'react-use/lib/useAsync';
const useStyles = makeStyles(theme => ({
infoCard: {
marginBottom: theme.spacing(3),
'& + .MuiAlert-root': {
marginTop: theme.spacing(3),
},
'& .MuiCardContent-root': {
padding: theme.spacing(2, 1, 2, 2),
},
},
readMe: {
overflowY: 'auto',
paddingRight: theme.spacing(1),
@@ -63,6 +54,35 @@ type Props = {
maxHeight?: number;
};
type ErrorProps = {
error: Error;
};
function isNotFoundError(error: any): boolean {
return error?.response?.status === 404;
}
const ReadmeCardError = ({ error }: ErrorProps) => {
if (isNotFoundError(error))
return (
<EmptyState
title="No README available for this entity"
missing="field"
description="You can add a README to your entity by following the Azure DevOps documentation."
action={
<Button
variant="contained"
color="primary"
href="https://docs.microsoft.com/en-us/azure/devops/repos/git/create-a-readme?view=azure-devops"
>
Read more
</Button>
}
/>
);
return <ErrorPanel title={error.message} error={error} />;
};
export const ReadmeCard = (props: Props) => {
const classes = useStyles();
const api = useApi(azureDevOpsApiRef);
@@ -81,52 +101,20 @@ export const ReadmeCard = (props: Props) => {
if (loading) {
return <Progress />;
} else if (error) {
return (
<Alert severity="error" className={classes.infoCard}>
{error.message}
</Alert>
);
return <ReadmeCardError error={error} />;
}
if (!value)
return (
<EmptyState
title="No README available for this entity"
missing="field"
description="You can add a README to your entity by following the Azure DevOps documentation."
action={
<Button
variant="contained"
color="primary"
href="https://docs.microsoft.com/en-us/azure/devops/repos/git/create-a-readme?view=azure-devops"
>
Read more
</Button>
}
/>
);
return (
<InfoCard
title="Readme"
className={classes.infoCard}
deepLink={{
link: value!.url,
title: 'Readme',
onClick: e => {
e.preventDefault();
window.open(value?.url);
},
}}
>
<div
className={classes.readMe}
style={{
maxHeight: `${props.maxHeight}px`,
}}
>
<Box className={classes.readMe} sx={{ maxHeight: props.maxHeight }}>
<MarkdownContent content={value?.content ?? ''} />
</div>
</Box>
</InfoCard>
);
};
+5 -4
View File
@@ -24,12 +24,12 @@ import {
azurePullRequestDashboardRouteRef,
azureGitTagsEntityContentRouteRef,
azurePullRequestsEntityContentRouteRef,
azureReadmeEntityCardRouteRef,
} from './routes';
import {
createApiFactory,
createPlugin,
createRoutableExtension,
createComponentExtension,
discoveryApiRef,
identityApiRef,
} from '@backstage/core-plugin-api';
@@ -103,9 +103,10 @@ export const EntityAzurePullRequestsContent = azureDevOpsPlugin.provide(
);
export const EntityAzureReadmeCard = azureDevOpsPlugin.provide(
createRoutableExtension({
createComponentExtension({
name: 'EntityAzureReadmeCard',
component: () => import('./components/ReadmeCard').then(m => m.ReadmeCard),
mountPoint: azureReadmeEntityCardRouteRef,
component: {
lazy: () => import('./components/ReadmeCard').then(m => m.ReadmeCard),
},
}),
);
-4
View File
@@ -31,7 +31,3 @@ export const azureGitTagsEntityContentRouteRef = createRouteRef({
export const azurePullRequestsEntityContentRouteRef = createRouteRef({
id: 'azure-pull-requests-entity-content',
});
export const azureReadmeEntityCardRouteRef = createRouteRef({
id: 'azure-readme-entity-card',
});