Merge branch 'master' into ryanv-cost-insights-custom-toolips
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog': patch
|
||||
---
|
||||
|
||||
Improved the edit link to open the component yaml in edit mode in corresponding SCM. Broke out logic for createEditLink to be reused.
|
||||
@@ -35,7 +35,7 @@
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@octokit/rest": "^18.0.0",
|
||||
"@roadiehq/backstage-plugin-github-insights": "^0.2.12",
|
||||
"@roadiehq/backstage-plugin-github-pull-requests": "^0.6.1",
|
||||
"@roadiehq/backstage-plugin-github-pull-requests": "^0.6.2",
|
||||
"@roadiehq/backstage-plugin-travis-ci": "^0.2.7",
|
||||
"history": "^5.0.0",
|
||||
"prop-types": "^15.7.2",
|
||||
|
||||
@@ -36,11 +36,13 @@
|
||||
"@types/cors": "^2.8.6",
|
||||
"@types/express": "^4.17.6",
|
||||
"compression": "^1.7.4",
|
||||
"concat-stream": "^2.0.0",
|
||||
"cors": "^2.8.5",
|
||||
"cross-fetch": "^3.0.6",
|
||||
"express": "^4.17.1",
|
||||
"express-prom-bundle": "^6.1.0",
|
||||
"express-promise-router": "^3.0.3",
|
||||
"fs-extra": "^9.0.1",
|
||||
"git-url-parse": "^11.4.0",
|
||||
"helmet": "^4.0.0",
|
||||
"knex": "^0.21.6",
|
||||
@@ -51,6 +53,7 @@
|
||||
"prom-client": "^12.0.0",
|
||||
"selfsigned": "^1.10.7",
|
||||
"stoppable": "^1.1.0",
|
||||
"tar": "^6.0.5",
|
||||
"winston": "^3.2.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@@ -64,17 +67,24 @@
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.2.0",
|
||||
"@types/compression": "^1.7.0",
|
||||
"@types/concat-stream": "^1.6.0",
|
||||
"@types/fs-extra": "^9.0.3",
|
||||
"@types/http-errors": "^1.6.3",
|
||||
"@types/minimist": "^1.2.0",
|
||||
"@types/mock-fs": "^4.13.0",
|
||||
"@types/morgan": "^1.9.0",
|
||||
"@types/recursive-readdir": "^2.2.0",
|
||||
"@types/stoppable": "^1.1.0",
|
||||
"@types/supertest": "^2.0.8",
|
||||
"@types/tar": "^4.0.3",
|
||||
"@types/webpack-env": "^1.15.2",
|
||||
"@types/yaml": "^1.9.7",
|
||||
"get-port": "^5.1.1",
|
||||
"http-errors": "^1.7.3",
|
||||
"jest": "^26.0.1",
|
||||
"mock-fs": "^4.13.0",
|
||||
"msw": "^0.21.2",
|
||||
"recursive-readdir": "^2.2.2",
|
||||
"supertest": "^4.0.2"
|
||||
},
|
||||
"files": [
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
*/
|
||||
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { msw } from '@backstage/test-utils';
|
||||
import { rest } from 'msw';
|
||||
import {
|
||||
getApiRequestOptions,
|
||||
getApiUrl,
|
||||
@@ -24,6 +27,10 @@ import {
|
||||
ProviderConfig,
|
||||
readConfig,
|
||||
} from './GithubUrlReader';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import mockfs from 'mock-fs';
|
||||
import recursive from 'recursive-readdir';
|
||||
|
||||
describe('GithubUrlReader', () => {
|
||||
describe('getApiRequestOptions', () => {
|
||||
@@ -230,4 +237,77 @@ describe('GithubUrlReader', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('readTree', () => {
|
||||
const worker = setupServer();
|
||||
|
||||
msw.setupDefaultHandlers(worker);
|
||||
|
||||
const repoBuffer = fs.readFileSync(
|
||||
path.resolve('src', 'reading', '__fixtures__', 'repo.tar.gz'),
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
worker.use(
|
||||
rest.get(
|
||||
'https://github.com/spotify/mock/archive/repo.tar.gz',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.set('Content-Type', 'application/x-gzip'),
|
||||
ctx.body(repoBuffer),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('returns the wanted files from an archive', async () => {
|
||||
const processor = new GithubUrlReader({
|
||||
host: 'github.com',
|
||||
});
|
||||
|
||||
const response = await processor.readTree(
|
||||
'https://github.com/spotify/mock',
|
||||
'repo',
|
||||
['mkdocs.yml', 'docs'],
|
||||
);
|
||||
|
||||
const files = response.files();
|
||||
|
||||
const mkDocsFile = await files[0].content();
|
||||
const indexMarkdownFile = await files[1].content();
|
||||
|
||||
expect(mkDocsFile.toString()).toBe('site_name: Test\n');
|
||||
expect(indexMarkdownFile.toString()).toBe('# Test\n');
|
||||
});
|
||||
|
||||
it('returns a folder path from an archive', async () => {
|
||||
const processor = new GithubUrlReader({
|
||||
host: 'github.com',
|
||||
});
|
||||
|
||||
const response = await processor.readTree(
|
||||
'https://github.com/spotify/mock',
|
||||
'repo',
|
||||
['mkdocs.yml', 'docs'],
|
||||
);
|
||||
|
||||
mockfs();
|
||||
const directory = await response.dir('/tmp/fs');
|
||||
|
||||
const writtenToDirectory = fs.existsSync(directory);
|
||||
const paths = await recursive(directory);
|
||||
mockfs.restore();
|
||||
|
||||
expect(writtenToDirectory).toBe(true);
|
||||
expect(paths.sort()).toEqual(
|
||||
[
|
||||
'/tmp/fs/mock-repo/docs/index.md',
|
||||
'/tmp/fs/mock-repo/mkdocs.yml',
|
||||
].sort(),
|
||||
);
|
||||
|
||||
worker.resetHandlers();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,7 +18,12 @@ import { Config } from '@backstage/config';
|
||||
import parseGitUri from 'git-url-parse';
|
||||
import fetch from 'cross-fetch';
|
||||
import { NotFoundError } from '../errors';
|
||||
import { ReaderFactory, UrlReader } from './types';
|
||||
import { ReaderFactory, ReadTreeResponse, UrlReader, File } from './types';
|
||||
import tar from 'tar';
|
||||
import fs from 'fs-extra';
|
||||
import concatStream from 'concat-stream';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
|
||||
/**
|
||||
* The configuration parameters for a single GitHub API provider.
|
||||
@@ -229,6 +234,92 @@ export class GithubUrlReader implements UrlReader {
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
private async getRepositoryArchive(
|
||||
repoUrl: string,
|
||||
branchName: string,
|
||||
): Promise<Response> {
|
||||
return fetch(new URL(`${repoUrl}/archive/${branchName}.tar.gz`).toString());
|
||||
}
|
||||
|
||||
private async writeBufferToFile(
|
||||
filePath: string,
|
||||
content: Buffer,
|
||||
): Promise<void> {
|
||||
await fs.outputFile(filePath, content.toString());
|
||||
}
|
||||
|
||||
async readTree(
|
||||
repoUrl: string,
|
||||
branchName: string,
|
||||
paths: Array<string>,
|
||||
): Promise<ReadTreeResponse> {
|
||||
const { name: repoName } = parseGitUri(repoUrl);
|
||||
|
||||
const repoArchive = await this.getRepositoryArchive(repoUrl, branchName);
|
||||
|
||||
const files: File[] = [];
|
||||
return new Promise(resolve => {
|
||||
const parser = new (tar.Parse as any)({
|
||||
filter: (path: string) =>
|
||||
!!paths.filter(file => {
|
||||
return path.startsWith(`${repoName}-${branchName}/${file}`);
|
||||
}).length,
|
||||
onentry: (entry: tar.ReadEntry) => {
|
||||
if (entry.type === 'Directory') {
|
||||
entry.resume();
|
||||
return;
|
||||
}
|
||||
|
||||
const contentPromise: Promise<Buffer> = new Promise(res => {
|
||||
entry.pipe(concatStream(res));
|
||||
});
|
||||
|
||||
files.push({
|
||||
path: entry.path,
|
||||
content: () => contentPromise,
|
||||
});
|
||||
|
||||
entry.resume();
|
||||
},
|
||||
});
|
||||
|
||||
// @ts-ignore Typescript doesn't consider .pipe a method on ReadableStream. Don't know why.
|
||||
repoArchive.body?.pipe(parser).on('finish', () => {
|
||||
resolve({
|
||||
files: () => {
|
||||
return files;
|
||||
},
|
||||
archive: () => {
|
||||
return new Promise(resolve =>
|
||||
resolve(Buffer.from('Archive is not yet implemented')),
|
||||
);
|
||||
},
|
||||
dir: (outDir: string | undefined) => {
|
||||
const targetDirectory =
|
||||
outDir || fs.mkdtempSync(path.join(os.tmpdir(), 'backstage-'));
|
||||
|
||||
return new Promise((res, rej) => {
|
||||
Promise.all(
|
||||
files.map(async file => {
|
||||
return this.writeBufferToFile(
|
||||
`${targetDirectory}/${file.path}`,
|
||||
await file.content(),
|
||||
);
|
||||
}),
|
||||
)
|
||||
.then(() => {
|
||||
res(`${targetDirectory}/${repoName}-${branchName}`);
|
||||
})
|
||||
.catch(err => {
|
||||
rej(err);
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
toString() {
|
||||
const { host, token } = this.config;
|
||||
return `github{host=${host},authed=${Boolean(token)}}`;
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { UrlReader, UrlReaderPredicateTuple } from './types';
|
||||
import { ReadTreeResponse, UrlReader, UrlReaderPredicateTuple } from './types';
|
||||
|
||||
type Options = {
|
||||
// UrlReader to fall back to if no other reader is matched
|
||||
@@ -53,6 +53,33 @@ export class UrlReaderPredicateMux implements UrlReader {
|
||||
throw new Error(`No reader found that could handle '${url}'`);
|
||||
}
|
||||
|
||||
readTree(
|
||||
repoUrl: string,
|
||||
branchName: string,
|
||||
paths: Array<string>,
|
||||
): Promise<ReadTreeResponse> {
|
||||
const parsed = new URL(repoUrl);
|
||||
|
||||
for (const { predicate, reader } of this.readers) {
|
||||
if (predicate(parsed)) {
|
||||
if (reader.readTree) return reader.readTree(repoUrl, branchName, paths);
|
||||
throw new Error(
|
||||
`Trying to call readTree on UrlReader which does not support the feature.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.fallback) {
|
||||
if (this.fallback.readTree)
|
||||
return this.fallback.readTree(repoUrl, branchName, paths);
|
||||
throw new Error(
|
||||
`Trying to call readTree on UrlReader which does not support the feature.`,
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error(`No reader found that could handle '${repoUrl}'`);
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `predicateMux{readers=${this.readers
|
||||
.map(t => t.reader)
|
||||
|
||||
Binary file not shown.
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export type { UrlReader } from './types';
|
||||
export type { UrlReader, ReadTreeResponse } from './types';
|
||||
export { UrlReaders } from './UrlReaders';
|
||||
export { AzureUrlReader } from './AzureUrlReader';
|
||||
export { BitbucketUrlReader } from './BitbucketUrlReader';
|
||||
|
||||
@@ -22,6 +22,11 @@ import { Config } from '@backstage/config';
|
||||
*/
|
||||
export type UrlReader = {
|
||||
read(url: string): Promise<Buffer>;
|
||||
readTree?(
|
||||
repoUrl: string,
|
||||
branchName: string,
|
||||
paths: Array<string>,
|
||||
): Promise<ReadTreeResponse>;
|
||||
};
|
||||
|
||||
export type UrlReaderPredicateTuple = {
|
||||
@@ -37,3 +42,14 @@ export type ReaderFactory = (options: {
|
||||
config: Config;
|
||||
logger: Logger;
|
||||
}) => UrlReaderPredicateTuple[];
|
||||
|
||||
export type File = {
|
||||
path: string;
|
||||
content(): Promise<Buffer>;
|
||||
};
|
||||
|
||||
export type ReadTreeResponse = {
|
||||
files(): File[];
|
||||
archive(): Promise<Buffer>;
|
||||
dir(outDir?: string): Promise<string>;
|
||||
};
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
LocalPublish,
|
||||
TechdocsGenerator,
|
||||
CommonGitPreparer,
|
||||
UrlPreparer,
|
||||
} from '@backstage/plugin-techdocs-backend';
|
||||
import { PluginEnvironment } from '../types';
|
||||
import Docker from 'dockerode';
|
||||
@@ -30,20 +31,25 @@ export default async function createPlugin({
|
||||
logger,
|
||||
config,
|
||||
discovery,
|
||||
reader,
|
||||
}: PluginEnvironment) {
|
||||
const generators = new Generators();
|
||||
const techdocsGenerator = new TechdocsGenerator(logger, config);
|
||||
generators.register('techdocs', techdocsGenerator);
|
||||
|
||||
const preparers = new Preparers();
|
||||
const commonGitPreparer = new CommonGitPreparer(logger);
|
||||
|
||||
const directoryPreparer = new DirectoryPreparer(logger);
|
||||
preparers.register('dir', directoryPreparer);
|
||||
|
||||
const commonGitPreparer = new CommonGitPreparer(logger);
|
||||
preparers.register('github', commonGitPreparer);
|
||||
preparers.register('gitlab', commonGitPreparer);
|
||||
preparers.register('azure/api', commonGitPreparer);
|
||||
|
||||
const urlPreparer = new UrlPreparer(reader, logger);
|
||||
preparers.register('url', urlPreparer);
|
||||
|
||||
const publisher = new LocalPublish(logger);
|
||||
|
||||
const dockerClient = new Docker();
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
"@types/react": "^16.9",
|
||||
"classnames": "^2.2.6",
|
||||
"git-url-parse": "^11.4.0",
|
||||
"moment": "^2.26.0",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
|
||||
@@ -18,7 +18,7 @@ import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { AboutCard } from './AboutCard';
|
||||
|
||||
describe('<AboutCard />', () => {
|
||||
describe('<AboutCard /> GitHub', () => {
|
||||
it('renders info and "view source" link', () => {
|
||||
const entity = {
|
||||
apiVersion: 'v1',
|
||||
@@ -42,5 +42,71 @@ describe('<AboutCard />', () => {
|
||||
'href',
|
||||
'https://github.com/backstage/backstage/blob/master/software.yaml',
|
||||
);
|
||||
expect(getByText('View Source').closest('a')).toHaveAttribute(
|
||||
'edithref',
|
||||
'https://github.com/backstage/backstage/edit/master/software.yaml',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('<AboutCard /> GitLab', () => {
|
||||
it('renders info and "view source" link', () => {
|
||||
const entity = {
|
||||
apiVersion: 'v1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'software',
|
||||
annotations: {
|
||||
'backstage.io/managed-by-location':
|
||||
'gitlab:https://gitlab.com/backstage/backstage/-/blob/master/software.yaml',
|
||||
},
|
||||
},
|
||||
spec: {
|
||||
owner: 'guest',
|
||||
type: 'service',
|
||||
lifecycle: 'production',
|
||||
},
|
||||
};
|
||||
const { getByText } = render(<AboutCard entity={entity} />);
|
||||
expect(getByText('service')).toBeInTheDocument();
|
||||
expect(getByText('View Source').closest('a')).toHaveAttribute(
|
||||
'href',
|
||||
'https://gitlab.com/backstage/backstage/-/blob/master/software.yaml',
|
||||
);
|
||||
expect(getByText('View Source').closest('a')).toHaveAttribute(
|
||||
'edithref',
|
||||
'https://gitlab.com/backstage/backstage/-/edit/master/software.yaml',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('<AboutCard /> BitBucket', () => {
|
||||
it('renders info and "view source" link', () => {
|
||||
const entity = {
|
||||
apiVersion: 'v1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'software',
|
||||
annotations: {
|
||||
'backstage.io/managed-by-location':
|
||||
'bitbucket:https://bitbucket.org/backstage/backstage/src/master/software.yaml',
|
||||
},
|
||||
},
|
||||
spec: {
|
||||
owner: 'guest',
|
||||
type: 'service',
|
||||
lifecycle: 'production',
|
||||
},
|
||||
};
|
||||
const { getByText } = render(<AboutCard entity={entity} />);
|
||||
expect(getByText('service')).toBeInTheDocument();
|
||||
expect(getByText('View Source').closest('a')).toHaveAttribute(
|
||||
'href',
|
||||
'https://bitbucket.org/backstage/backstage/src/master/software.yaml',
|
||||
);
|
||||
expect(getByText('View Source').closest('a')).toHaveAttribute(
|
||||
'edithref',
|
||||
'https://bitbucket.org/backstage/backstage/src/master/software.yaml?mode=edit&spa=0&at=master',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -37,6 +37,8 @@ import EditIcon from '@material-ui/icons/Edit';
|
||||
import GitHubIcon from '@material-ui/icons/GitHub';
|
||||
import React from 'react';
|
||||
import { IconLinkVertical } from './IconLinkVertical';
|
||||
import { findLocationForEntityMeta } from '../../data/utils';
|
||||
import { createEditLink, determineUrlType } from '../createEditLink';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
links: {
|
||||
@@ -79,18 +81,24 @@ const iconMap: Record<string, React.ReactNode> = {
|
||||
github: <GitHubIcon />,
|
||||
};
|
||||
|
||||
type CodeLinkInfo = { icon?: React.ReactNode; href?: string };
|
||||
type CodeLinkInfo = {
|
||||
icon?: React.ReactNode;
|
||||
edithref?: string;
|
||||
href?: string;
|
||||
};
|
||||
|
||||
function getCodeLinkInfo(entity: Entity): CodeLinkInfo {
|
||||
const location =
|
||||
entity?.metadata?.annotations?.['backstage.io/managed-by-location'];
|
||||
|
||||
const location = findLocationForEntityMeta(entity?.metadata);
|
||||
if (location) {
|
||||
// split by first `:`
|
||||
// e.g. "github:https://github.com/backstage/backstage/blob/master/software.yaml"
|
||||
const [type, target] = location.split(/:(.+)/);
|
||||
|
||||
return { icon: iconMap[type], href: target };
|
||||
const type =
|
||||
location.type === 'url'
|
||||
? determineUrlType(location.target)
|
||||
: location.type;
|
||||
return {
|
||||
icon: iconMap[type],
|
||||
edithref: createEditLink(location),
|
||||
href: location.target,
|
||||
};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
@@ -109,7 +117,12 @@ export function AboutCard({ entity, variant }: AboutCardProps) {
|
||||
<CardHeader
|
||||
title="About"
|
||||
action={
|
||||
<IconButton href={codeLink.href || '#'} aria-label="Edit">
|
||||
<IconButton
|
||||
aria-label="Edit"
|
||||
onClick={() => {
|
||||
window.open(codeLink.edithref || '#', '_blank');
|
||||
}}
|
||||
>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { Entity, LocationSpec } from '@backstage/catalog-model';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { Table, TableColumn, TableProps } from '@backstage/core';
|
||||
import { Chip, Link } from '@material-ui/core';
|
||||
import Edit from '@material-ui/icons/Edit';
|
||||
@@ -22,6 +22,7 @@ import { Alert } from '@material-ui/lab';
|
||||
import React from 'react';
|
||||
import { generatePath, Link as RouterLink } from 'react-router-dom';
|
||||
import { findLocationForEntityMeta } from '../../data/utils';
|
||||
import { createEditLink } from '../createEditLink';
|
||||
import { useStarredEntities } from '../../hooks/useStarredEntities';
|
||||
import { entityRoute, entityRouteParams } from '../../routes';
|
||||
import {
|
||||
@@ -120,14 +121,6 @@ export const CatalogTable = ({
|
||||
};
|
||||
},
|
||||
(rowData: Entity) => {
|
||||
const createEditLink = (location: LocationSpec): string => {
|
||||
switch (location.type) {
|
||||
case 'github':
|
||||
return location.target.replace('/blob/', '/edit/');
|
||||
default:
|
||||
return location.target;
|
||||
}
|
||||
};
|
||||
const location = findLocationForEntityMeta(rowData.metadata);
|
||||
return {
|
||||
icon: () => <Edit fontSize="small" />,
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { LocationSpec } from '@backstage/catalog-model';
|
||||
import gitUrlParse from 'git-url-parse';
|
||||
|
||||
/**
|
||||
* Creates the edit link for components yaml file
|
||||
* @see LocationSpec
|
||||
* @param location The LocationSpec being used to determine entity SCM location
|
||||
* @returns string representing the edit location based on SCM path
|
||||
*/
|
||||
|
||||
export const createEditLink = (location: LocationSpec): string | undefined => {
|
||||
try {
|
||||
const urlData = gitUrlParse(location.target);
|
||||
const url = new URL(location.target);
|
||||
switch (location.type) {
|
||||
case 'github':
|
||||
case 'gitlab':
|
||||
return location.target.replace('/blob/', '/edit/');
|
||||
case 'bitbucket':
|
||||
url.searchParams.set('mode', 'edit');
|
||||
url.searchParams.set('spa', '0');
|
||||
url.searchParams.set('at', urlData.ref);
|
||||
return url.toString();
|
||||
case 'url':
|
||||
if (
|
||||
urlData.source === 'github.com' ||
|
||||
urlData.source === 'gitlab.com/'
|
||||
) {
|
||||
return location.target.replace('/blob/', '/edit/');
|
||||
} else if (urlData.source === 'bitbucket.org') {
|
||||
url.searchParams.set('mode', 'edit');
|
||||
url.searchParams.set('spa', '0');
|
||||
url.searchParams.set('at', urlData.ref);
|
||||
return url.toString();
|
||||
}
|
||||
return location.target;
|
||||
default:
|
||||
return location.target;
|
||||
}
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines type based on passed in url. This is used to set the icon associated with the type of entity
|
||||
* @param url
|
||||
* @returns string representing type of icon to be used
|
||||
*/
|
||||
export const determineUrlType = (url: string): string => {
|
||||
const urlData = gitUrlParse(url);
|
||||
|
||||
if (urlData.source === 'github.com') {
|
||||
return 'github';
|
||||
} else if (urlData.source === 'bitbucket.org') {
|
||||
return 'bitbucket';
|
||||
} else if (urlData.source === 'gitlab.com') {
|
||||
return 'gitlab';
|
||||
}
|
||||
return 'url';
|
||||
};
|
||||
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { getDocFilesFromRepository } from './helpers';
|
||||
import { UrlReader, ReadTreeResponse } from '@backstage/backend-common';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
|
||||
describe('getDocFilesFromRepository', () => {
|
||||
it('should take the directory from UrlReader.readTree and add the docs path when mkdocs.yml is in root', async () => {
|
||||
class MockUrlReader implements UrlReader {
|
||||
async read() {
|
||||
return Buffer.from('mock');
|
||||
}
|
||||
|
||||
async readTree(): Promise<ReadTreeResponse> {
|
||||
return {
|
||||
dir: async () => {
|
||||
return '/tmp/testfolder';
|
||||
},
|
||||
files: () => {
|
||||
return [];
|
||||
},
|
||||
archive: async () => {
|
||||
return Buffer.from('');
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const mockEntity: Entity = {
|
||||
metadata: {
|
||||
namespace: 'default',
|
||||
annotations: {
|
||||
'backstage.io/techdocs-ref':
|
||||
'url:https://github.com/backstage/backstage/blob/master/mkdocs.yml',
|
||||
},
|
||||
name: 'mytestcomponent',
|
||||
description: 'A component for testing',
|
||||
},
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
spec: {
|
||||
type: 'documentation',
|
||||
lifecycle: 'experimental',
|
||||
owner: 'testuser',
|
||||
},
|
||||
};
|
||||
|
||||
const output = await getDocFilesFromRepository(
|
||||
new MockUrlReader(),
|
||||
mockEntity,
|
||||
);
|
||||
|
||||
expect(output).toBe('/tmp/testfolder/.');
|
||||
});
|
||||
|
||||
it('should take the directory from UrlReader.readTree and add the docs path when mkdocs.yml is in a subfolder', async () => {
|
||||
class MockUrlReader implements UrlReader {
|
||||
async read() {
|
||||
return Buffer.from('mock');
|
||||
}
|
||||
|
||||
async readTree(): Promise<ReadTreeResponse> {
|
||||
return {
|
||||
dir: async () => {
|
||||
return '/tmp/testfolder';
|
||||
},
|
||||
files: () => {
|
||||
return [];
|
||||
},
|
||||
archive: async () => {
|
||||
return Buffer.from('');
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const mockEntity: Entity = {
|
||||
metadata: {
|
||||
namespace: 'default',
|
||||
annotations: {
|
||||
'backstage.io/techdocs-ref':
|
||||
'url:https://github.com/backstage/backstage/blob/master/subfolder/mkdocs.yml',
|
||||
},
|
||||
name: 'mytestcomponent',
|
||||
description: 'A component for testing',
|
||||
},
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
spec: {
|
||||
type: 'documentation',
|
||||
lifecycle: 'experimental',
|
||||
owner: 'testuser',
|
||||
},
|
||||
};
|
||||
|
||||
const output = await getDocFilesFromRepository(
|
||||
new MockUrlReader(),
|
||||
mockEntity,
|
||||
);
|
||||
|
||||
expect(output).toBe('/tmp/testfolder/subfolder');
|
||||
});
|
||||
});
|
||||
@@ -22,7 +22,7 @@ import fs from 'fs-extra';
|
||||
import { getDefaultBranch } from './default-branch';
|
||||
import { getGitRepoType, getTokenForGitRepo } from './git-auth';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { InputError } from '@backstage/backend-common';
|
||||
import { InputError, UrlReader } from '@backstage/backend-common';
|
||||
import { RemoteProtocol } from './techdocs/stages/prepare/types';
|
||||
import { Logger } from 'winston';
|
||||
|
||||
@@ -78,6 +78,7 @@ export const getLocationForEntity = (
|
||||
case 'github':
|
||||
case 'gitlab':
|
||||
case 'azure/api':
|
||||
case 'url':
|
||||
return { type, target };
|
||||
case 'dir':
|
||||
if (path.isAbsolute(target)) return { type, target };
|
||||
@@ -168,3 +169,34 @@ export const getLastCommitTimestamp = async (
|
||||
|
||||
return commit.date().getTime();
|
||||
};
|
||||
|
||||
export const getDocFilesFromRepository = async (
|
||||
reader: UrlReader,
|
||||
entity: Entity,
|
||||
): Promise<any> => {
|
||||
const { target } = parseReferenceAnnotation(
|
||||
'backstage.io/techdocs-ref',
|
||||
entity,
|
||||
);
|
||||
|
||||
const { ref, filepath: mkdocsPath } = parseGitUrl(target);
|
||||
|
||||
const docsRootPath = path.dirname(mkdocsPath);
|
||||
const docsFolderPath = path.join(docsRootPath, 'docs');
|
||||
|
||||
if (reader.readTree) {
|
||||
const readTreeResponse = await reader.readTree(
|
||||
parseGitUrl(target).toString(),
|
||||
ref,
|
||||
[mkdocsPath, docsFolderPath],
|
||||
);
|
||||
|
||||
const tmpDir = await readTreeResponse.dir();
|
||||
|
||||
return `${tmpDir}/${docsRootPath}`;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`No readTree method available on the UrlReader for ${target}`,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -103,7 +103,7 @@ export class DocsBuilder {
|
||||
const { type, target } = getLocationForEntity(this.entity);
|
||||
|
||||
// Unless docs are stored locally
|
||||
const nonAgeCheckTypes = ['dir', 'file'];
|
||||
const nonAgeCheckTypes = ['dir', 'file', 'url'];
|
||||
if (!nonAgeCheckTypes.includes(type)) {
|
||||
const lastCommit = await getLastCommitTimestamp(target, this.logger);
|
||||
const storageTimeStamp = buildMetadataStorage.getTimestamp();
|
||||
|
||||
@@ -15,5 +15,6 @@
|
||||
*/
|
||||
export { DirectoryPreparer } from './dir';
|
||||
export { CommonGitPreparer } from './commonGit';
|
||||
export { UrlPreparer } from './url';
|
||||
export { Preparers } from './preparers';
|
||||
export type { PreparerBuilder, PreparerBase } from './types';
|
||||
|
||||
@@ -30,4 +30,10 @@ export type PreparerBuilder = {
|
||||
get(entity: Entity): PreparerBase;
|
||||
};
|
||||
|
||||
export type RemoteProtocol = 'dir' | 'github' | 'gitlab' | 'file' | 'azure/api';
|
||||
export type RemoteProtocol =
|
||||
| 'dir'
|
||||
| 'github'
|
||||
| 'gitlab'
|
||||
| 'file'
|
||||
| 'azure/api'
|
||||
| 'url';
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { Entity } from '@backstage/catalog-model';
|
||||
import { PreparerBase } from './types';
|
||||
import { getDocFilesFromRepository } from '../../../helpers';
|
||||
|
||||
import { Logger } from 'winston';
|
||||
import { UrlReader } from '@backstage/backend-common';
|
||||
|
||||
export class UrlPreparer implements PreparerBase {
|
||||
private readonly logger: Logger;
|
||||
private readonly reader: UrlReader;
|
||||
|
||||
constructor(reader: UrlReader, logger: Logger) {
|
||||
this.logger = logger;
|
||||
this.reader = reader;
|
||||
}
|
||||
|
||||
async prepare(entity: Entity): Promise<string> {
|
||||
try {
|
||||
return getDocFilesFromRepository(this.reader, entity);
|
||||
} catch (error) {
|
||||
this.logger.debug(
|
||||
`Unable to fetch files for building docs ${error.message}`,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3672,10 +3672,10 @@
|
||||
react-use "^15.3.3"
|
||||
remark-gfm "^1.0.0"
|
||||
|
||||
"@roadiehq/backstage-plugin-github-pull-requests@^0.6.1":
|
||||
version "0.6.1"
|
||||
resolved "https://registry.npmjs.org/@roadiehq/backstage-plugin-github-pull-requests/-/backstage-plugin-github-pull-requests-0.6.1.tgz#bce3ce93af7b2f6c38d4665c8a0d8a6b2eaa4212"
|
||||
integrity sha512-3r0BBV6kRlbrlfVA/G7Tc0xbw9A+9roihspCaV/yhhqXgk/ik5v6ftEautlx45+gKaFMp+jgmfKHqFgOdSyhZw==
|
||||
"@roadiehq/backstage-plugin-github-pull-requests@^0.6.2":
|
||||
version "0.6.2"
|
||||
resolved "https://registry.npmjs.org/@roadiehq/backstage-plugin-github-pull-requests/-/backstage-plugin-github-pull-requests-0.6.2.tgz#02f4a7a03e1a7dc24342ec695130017ce7d3ad8d"
|
||||
integrity sha512-OdBWO6NdiBKlol1WlbFQGeHxV2C8wl8EiI2NgA15lozsqFA22kWis6Z16cAHe4ZdRCjihDmqINtAIJzeZZ1gOg==
|
||||
dependencies:
|
||||
"@backstage/catalog-model" "^0.2.0"
|
||||
"@backstage/core" "^0.2.0"
|
||||
@@ -4916,6 +4916,13 @@
|
||||
dependencies:
|
||||
"@types/express" "*"
|
||||
|
||||
"@types/concat-stream@^1.6.0":
|
||||
version "1.6.0"
|
||||
resolved "https://registry.npmjs.org/@types/concat-stream/-/concat-stream-1.6.0.tgz#394dbe0bb5fee46b38d896735e8b68ef2390d00d"
|
||||
integrity sha1-OU2+C7X+5Gs42JZzXoto7yOQ0A0=
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/connect-history-api-fallback@*":
|
||||
version "1.3.3"
|
||||
resolved "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.3.3.tgz#4772b79b8b53185f0f4c9deab09236baf76ee3b4"
|
||||
@@ -5109,6 +5116,13 @@
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/fs-extra@^9.0.3":
|
||||
version "9.0.3"
|
||||
resolved "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.3.tgz#9996e5cce993508c32325380b429f04a1327523e"
|
||||
integrity sha512-NKdGoXLTFTRED3ENcfCsH8+ekV4gbsysanx2OPbstXVV6fZMgUCqTxubs6I9r7pbOJbFgVq1rpFtLURjKCZWUw==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/git-url-parse@^9.0.0":
|
||||
version "9.0.0"
|
||||
resolved "https://registry.npmjs.org/@types/git-url-parse/-/git-url-parse-9.0.0.tgz#aac1315a44fa4ed5a52c3820f6c3c2fb79cbd12d"
|
||||
@@ -22456,7 +22470,7 @@ tar@^4, tar@^4.4.10, tar@^4.4.12, tar@^4.4.8:
|
||||
safe-buffer "^5.1.2"
|
||||
yallist "^3.0.3"
|
||||
|
||||
tar@^6.0.1, tar@^6.0.2:
|
||||
tar@^6.0.1, tar@^6.0.2, tar@^6.0.5:
|
||||
version "6.0.5"
|
||||
resolved "https://registry.npmjs.org/tar/-/tar-6.0.5.tgz#bde815086e10b39f1dcd298e89d596e1535e200f"
|
||||
integrity sha512-0b4HOimQHj9nXNEAA7zWwMM91Zhhba3pspja6sQbgTpynOJf+bkjBnfybNYzbpLbnwXnbyB4LOREvlyXLkCHSg==
|
||||
|
||||
Reference in New Issue
Block a user