Merge pull request #4702 from SDA-SE/feat/annotations-view-edit-source

Move logic for generating URLs for the view, edit and source links of catalog entities from the catalog frontend into the backend
This commit is contained in:
Oliver Sand
2021-03-08 12:40:19 +01:00
committed by GitHub
28 changed files with 674 additions and 331 deletions
@@ -88,4 +88,10 @@ describe('ScmIntegrations', () => {
}),
).toBe('https://absolute.com/path');
});
it('can resolveEditUrl using fallback', () => {
expect(i.resolveEditUrl('http://example.com/x/a.yaml')).toBe(
'http://example.com/x/a.yaml',
);
});
});
@@ -91,4 +91,13 @@ export class ScmIntegrations implements ScmIntegrationRegistry {
return integration.resolveUrl(options);
}
resolveEditUrl(url: string): string {
const integration = this.byUrl(url);
if (!integration) {
return url;
}
return integration.resolveEditUrl(url);
}
}
@@ -99,4 +99,18 @@ describe('AzureIntegration', () => {
).toBe('https://dev.azure.com/organization/project/test');
});
});
it('resolve edit URL', () => {
const integration = new AzureIntegration({ host: 'h.com' } as any);
// TODO: The Azure integration doesn't support resolving an edit URL yet,
// instead we keep the input URL.
expect(
integration.resolveEditUrl(
'https://dev.azure.com/organization/project/_git/repository?path=%2Fcatalog-info.yaml',
),
).toBe(
'https://dev.azure.com/organization/project/_git/repository?path=%2Fcatalog-info.yaml',
);
});
});
@@ -15,7 +15,7 @@
*/
import parseGitUrl from 'git-url-parse';
import { basicIntegrations } from '../helpers';
import { basicIntegrations, isValidUrl } from '../helpers';
import { ScmIntegration, ScmIntegrationsFactory } from '../types';
import { AzureIntegrationConfig, readAzureIntegrationConfigs } from './config';
@@ -53,12 +53,8 @@ export class AzureIntegration implements ScmIntegration {
const { url, base } = options;
// If we can parse the url, it is absolute - then return it verbatim
try {
// eslint-disable-next-line no-new
new URL(url);
if (isValidUrl(url)) {
return url;
} catch {
// Ignore intentionally - looks like a relative path
}
const parsed = parseGitUrl(base);
@@ -78,4 +74,10 @@ export class AzureIntegration implements ScmIntegration {
return newUrl.toString();
}
resolveEditUrl(url: string): string {
// TODO: Implement edit URL for Azure, fallback to view url as I don't know
// how azure works.
return url;
}
}
@@ -44,4 +44,16 @@ describe('BitbucketIntegration', () => {
expect(integration.type).toBe('bitbucket');
expect(integration.title).toBe('h.com');
});
it('resolve edit URL', () => {
const integration = new BitbucketIntegration({ host: 'h.com' } as any);
expect(
integration.resolveEditUrl(
'https://bitbucket.org/my-owner/my-project/src/master/README.md',
),
).toBe(
'https://bitbucket.org/my-owner/my-project/src/master/README.md?mode=edit&spa=0&at=master',
);
});
});
@@ -14,6 +14,7 @@
* limitations under the License.
*/
import parseGitUrl from 'git-url-parse';
import { basicIntegrations, defaultScmResolveUrl } from '../helpers';
import { ScmIntegration, ScmIntegrationsFactory } from '../types';
import {
@@ -51,4 +52,16 @@ export class BitbucketIntegration implements ScmIntegration {
resolveUrl(options: { url: string; base: string }): string {
return defaultScmResolveUrl(options);
}
resolveEditUrl(url: string): string {
const urlData = parseGitUrl(url);
const editUrl = new URL(url);
editUrl.searchParams.set('mode', 'edit');
// TODO: Not sure what spa=0 does, at least bitbucket.org doesn't support it
// but this is taken over from the initial implementation.
editUrl.searchParams.set('spa', '0');
editUrl.searchParams.set('at', urlData.ref);
return editUrl.toString();
}
}
@@ -15,7 +15,7 @@
*/
import { ConfigReader } from '@backstage/config';
import { GitHubIntegration } from './GitHubIntegration';
import { GitHubIntegration, replaceUrlType } from './GitHubIntegration';
describe('GitHubIntegration', () => {
it('has a working factory', () => {
@@ -49,4 +49,60 @@ describe('GitHubIntegration', () => {
expect(integration.title).toBe('h.com');
expect(integration.config.host).toBe('h.com');
});
it('resolveUrl', () => {
const integration = new GitHubIntegration({ host: 'h.com' });
expect(
integration.resolveUrl({
url: '../a.yaml',
base:
'https://github.com/backstage/backstage/blob/master/test/README.md',
}),
).toBe('https://github.com/backstage/backstage/tree/master/a.yaml');
expect(
integration.resolveUrl({
url: './',
base:
'https://github.com/backstage/backstage/blob/master/test/README.md',
}),
).toBe('https://github.com/backstage/backstage/tree/master/test/');
});
it('resolve edit URL', () => {
const integration = new GitHubIntegration({ host: 'h.com' });
expect(
integration.resolveEditUrl(
'https://github.com/backstage/backstage/blob/master/README.md',
),
).toBe('https://github.com/backstage/backstage/edit/master/README.md');
});
});
describe('replaceUrlType', () => {
it('should replace with expected type', () => {
expect(
replaceUrlType(
'https://github.com/backstage/backstage/blob/master/README.md',
'edit',
),
).toBe('https://github.com/backstage/backstage/edit/master/README.md');
expect(
replaceUrlType(
'https://github.com/webmodules/blob/blob/master/test',
'tree',
),
).toBe('https://github.com/webmodules/blob/tree/master/test');
expect(
replaceUrlType('https://github.com/blob/blob/blob/master/test', 'tree'),
).toBe('https://github.com/blob/blob/tree/master/test');
expect(
replaceUrlType(
'https://github.com/backstage/backstage/edit/tree/README.md',
'blob',
),
).toBe('https://github.com/backstage/backstage/blob/tree/README.md');
});
});
@@ -47,6 +47,25 @@ export class GitHubIntegration implements ScmIntegration {
}
resolveUrl(options: { url: string; base: string }): string {
return defaultScmResolveUrl(options);
// GitHub uses blob URLs for files and tree urls for directory listings. But
// there is a redirect from tree to blob for files, so we can always return
// tree urls here.
return replaceUrlType(defaultScmResolveUrl(options), 'tree');
}
resolveEditUrl(url: string): string {
return replaceUrlType(url, 'edit');
}
}
export function replaceUrlType(
url: string,
type: 'blob' | 'tree' | 'edit',
): string {
return url.replace(
/\/\/([^/]+)\/([^/]+)\/([^/]+)\/(blob|tree|edit)\//,
(_, host, owner, repo) => {
return `//${host}/${owner}/${repo}/${type}/`;
},
);
}
@@ -15,7 +15,7 @@
*/
import { ConfigReader } from '@backstage/config';
import { GitLabIntegration } from './GitLabIntegration';
import { GitLabIntegration, replaceUrlType } from './GitLabIntegration';
describe('GitLabIntegration', () => {
it('has a working factory', () => {
@@ -43,4 +43,43 @@ describe('GitLabIntegration', () => {
expect(integration.type).toBe('gitlab');
expect(integration.title).toBe('h.com');
});
it('resolve edit URL', () => {
const integration = new GitLabIntegration({ host: 'h.com' } as any);
expect(
integration.resolveEditUrl(
'https://gitlab.com/my-org/my-project/-/blob/develop/README.md',
),
).toBe('https://gitlab.com/my-org/my-project/-/edit/develop/README.md');
});
});
describe('replaceUrlType', () => {
it('should replace with expected type', () => {
expect(
replaceUrlType(
'https://gitlab.com/my-org/my-project/-/blob/develop/README.md',
'edit',
),
).toBe('https://gitlab.com/my-org/my-project/-/edit/develop/README.md');
expect(
replaceUrlType(
'https://gitlab.com/webmodules/blob/-/blob/develop/test',
'tree',
),
).toBe('https://gitlab.com/webmodules/blob/-/tree/develop/test');
expect(
replaceUrlType(
'https://gitlab.com/blob/blob/-/blob/develop/test',
'tree',
),
).toBe('https://gitlab.com/blob/blob/-/tree/develop/test');
expect(
replaceUrlType(
'https://gitlab.com/blob/blob/-/edit/develop/README.md',
'tree',
),
).toBe('https://gitlab.com/blob/blob/-/tree/develop/README.md');
});
});
@@ -49,4 +49,15 @@ export class GitLabIntegration implements ScmIntegration {
resolveUrl(options: { url: string; base: string }): string {
return defaultScmResolveUrl(options);
}
resolveEditUrl(url: string): string {
return replaceUrlType(url, 'edit');
}
}
export function replaceUrlType(
url: string,
type: 'blob' | 'tree' | 'edit',
): string {
return url.replace(/\/\-\/(blob|tree|edit)\//, `/-/${type}/`);
}
+26
View File
@@ -51,6 +51,19 @@ export interface ScmIntegration {
* @param options.base The base URL onto which this resolution happens
*/
resolveUrl(options: { url: string; base: string }): string;
/**
* Resolves the edit URL for a file within the SCM system.
*
* Most SCM systems have a web interface that allows viewing and editing files
* in the repository. The returned URL directly jumps into the edit mode for
* the file.
* If this is not possible, the integration can fall back to a URL to view
* the file in the web interface.
*
* @param url The absolute URL to the file that should be edited.
*/
resolveEditUrl(url: string): string;
}
/**
@@ -103,6 +116,19 @@ export interface ScmIntegrationRegistry
* @param options.base The base URL onto which this resolution happens
*/
resolveUrl(options: { url: string; base: string }): string;
/**
* Resolves the edit URL for a file within the SCM system.
*
* Most SCM systems have a web interface that allows viewing and editing files
* in the repository. The returned URL directly jumps into the edit mode for
* the file.
* If this is not possible, the integration can fall back to a URL to view
* the file in the web interface.
*
* @param url The absolute URL to the file that should be edited.
*/
resolveEditUrl(url: string): string;
}
export type ScmIntegrationsFactory<T extends ScmIntegration> = (options: {