diff --git a/.changeset/thick-lobsters-swim.md b/.changeset/thick-lobsters-swim.md
new file mode 100644
index 0000000000..3a377b7535
--- /dev/null
+++ b/.changeset/thick-lobsters-swim.md
@@ -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.
diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json
index 6d52c4722f..045230d3f9 100644
--- a/plugins/catalog/package.json
+++ b/plugins/catalog/package.json
@@ -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",
diff --git a/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx b/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx
index 2854af2f26..3038fc3c5c 100644
--- a/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx
+++ b/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx
@@ -18,7 +18,7 @@ import React from 'react';
import { render } from '@testing-library/react';
import { AboutCard } from './AboutCard';
-describe('', () => {
+describe(' GitHub', () => {
it('renders info and "view source" link', () => {
const entity = {
apiVersion: 'v1',
@@ -42,5 +42,71 @@ describe('', () => {
'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(' 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();
+ 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(' 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();
+ 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',
+ );
});
});
diff --git a/plugins/catalog/src/components/AboutCard/AboutCard.tsx b/plugins/catalog/src/components/AboutCard/AboutCard.tsx
index 74877b5a65..8da67d7493 100644
--- a/plugins/catalog/src/components/AboutCard/AboutCard.tsx
+++ b/plugins/catalog/src/components/AboutCard/AboutCard.tsx
@@ -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 = {
github: ,
};
-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) {
+ {
+ window.open(codeLink.edithref || '#', '_blank');
+ }}
+ >
}
diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx
index 731a8dedc9..15a220e839 100644
--- a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx
+++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx
@@ -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: () => ,
diff --git a/plugins/catalog/src/components/createEditLink.ts b/plugins/catalog/src/components/createEditLink.ts
new file mode 100644
index 0000000000..cc04d605f1
--- /dev/null
+++ b/plugins/catalog/src/components/createEditLink.ts
@@ -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';
+};