Made changes requested in the pull request
Signed-off-by: Alisson Fabiano <afabiano@eshopworld.com>
This commit is contained in:
@@ -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', () => {
|
||||
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
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
`;
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user