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'; +};