About card edit improvements (#2950)
* Update to Edit link in Card component. * prettier * new line * Added changeset * Actually run prettier. . . * Added functionality to determine source of url for icon choice. Fixed failed tests. * Added test case for new edithref attribute * Prettier * forgot a file * correct url substring sanitation * corrected url substring sanitation * Added handling of Bitbucket Cloud and Gitlab. * Added tests for each GitLab and BitBucket edits. * Update to leverage git-url-parse * update test to reflect owner change
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.
|
||||
@@ -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';
|
||||
};
|
||||
Reference in New Issue
Block a user