Add component for multiple links

Signed-off-by: James Turley <james.turley@signalmedia.co>
Signed-off-by: James Turley <jamesturley1905@googlemail.com>
This commit is contained in:
James Turley
2021-03-16 17:53:29 +00:00
committed by James Turley
parent b96d9c5677
commit baee14e5d6
6 changed files with 126 additions and 52 deletions
@@ -135,6 +135,36 @@
"output": {
"type": "object",
"description": "A templated object describing the outputs of the scaffolding task.",
"properties": {
"links": {
"type": "array",
"description": "A list of external hyperlinks, typically pointing to resources created or updated by the template",
"items": {
"type": "object",
"required": ["url"],
"properties": {
"url": {
"type": "string",
"description": "A url in a standard uri format.",
"examples": ["https://github.com/my-org/my-new-repo"],
"minLength": 1
},
"title": {
"type": "string",
"description": "A user friendly display name for the link.",
"examples": ["View new repo"],
"minLength": 1
},
"icon": {
"type": "string",
"description": "A key representing a visual icon to be displayed in the UI.",
"examples": ["dashboard"],
"minLength": 1
}
}
}
}
},
"additionalProperties": {
"type": "string"
}
+3
View File
@@ -15,6 +15,7 @@
*/
import { SvgIconProps } from '@material-ui/core';
import MuiMenuBookIcon from '@material-ui/icons/MenuBook';
import MuiBrokenImageIcon from '@material-ui/icons/BrokenImage';
import MuiChatIcon from '@material-ui/icons/Chat';
import MuiDashboardIcon from '@material-ui/icons/Dashboard';
@@ -32,6 +33,8 @@ import { IconComponent, IconComponentMap, SystemIconKey } from './types';
export const defaultSystemIcons: IconComponentMap = {
brokenImage: MuiBrokenImageIcon,
// To be confirmed: see https://github.com/backstage/backstage/issues/4970
catalog: MuiMenuBookIcon,
chat: MuiChatIcon,
dashboard: MuiDashboardIcon,
email: MuiEmailIcon,
@@ -22,28 +22,24 @@ import Step from '@material-ui/core/Step';
import StepLabel from '@material-ui/core/StepLabel';
import Grid from '@material-ui/core/Grid';
import Typography from '@material-ui/core/Typography';
import { generatePath, useParams } from 'react-router';
import { useParams } from 'react-router';
import { useTaskEventStream } from '../hooks/useEventStream';
import LazyLog from 'react-lazylog/build/LazyLog';
import { Link } from 'react-router-dom';
import {
Box,
Button,
CircularProgress,
Paper,
StepButton,
StepIconProps,
} from '@material-ui/core';
import { Status } from '../../types';
import { Status, TaskOutput } from '../../types';
import { DateTime, Interval } from 'luxon';
import { useInterval } from 'react-use';
import Check from '@material-ui/icons/Check';
import Cancel from '@material-ui/icons/Cancel';
import FiberManualRecordIcon from '@material-ui/icons/FiberManualRecord';
import { entityRoute } from '@backstage/plugin-catalog-react';
import { parseEntityName } from '@backstage/catalog-model';
import classNames from 'classnames';
import { BackstageTheme } from '@backstage/theme';
import { TaskPageLinks } from './TaskPageLinks';
// typings are wrong for this library, so fallback to not parsing types.
const humanizeDuration = require('humanize-duration');
@@ -211,6 +207,9 @@ const TaskLogger = memo(({ log }: { log: string }) => {
);
});
const hasLinks = ({ entityRef, remoteUrl, links = [] }: TaskOutput): boolean =>
!!(entityRef || remoteUrl || links.length > 0);
export const TaskPage = () => {
const [userSelectedStepId, setUserSelectedStepId] = useState<
string | undefined
@@ -261,8 +260,8 @@ export const TaskPage = () => {
taskStream.loading === false &&
!taskStream.task;
const entityRef = taskStream.output?.entityRef;
const remoteUrl = taskStream.output?.remoteUrl;
const { output } = taskStream;
return (
<Page themeId="home">
<Header
@@ -291,38 +290,8 @@ export const TaskPage = () => {
currentStepId={currentStepId}
onUserStepChange={setUserSelectedStepId}
/>
{(entityRef || remoteUrl) && (
<Box
px={3}
pb={3}
display="flex"
flex={1}
justifyContent="space-between"
flexDirection="row"
>
{entityRef && (
<Button
size="small"
variant="outlined"
component={Link}
to={generatePath(
`/catalog/${entityRoute.path}`,
parseEntityName(entityRef),
)}
>
Open in catalog
</Button>
)}
{remoteUrl && (
<Button
size="small"
variant="outlined"
href={remoteUrl}
>
Repo
</Button>
)}
</Box>
{output && hasLinks(output) && (
<TaskPageLinks output={output} />
)}
</Paper>
</Grid>
@@ -0,0 +1,69 @@
/*
* Copyright 2021 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 React from 'react';
import { parseEntityName } from '@backstage/catalog-model';
import { IconComponent, IconKey, IconLink, useApp } from '@backstage/core';
import { entityRoute } from '@backstage/plugin-catalog-react';
import { Box } from '@material-ui/core';
import LanguageIcon from '@material-ui/icons/Language';
import { generatePath } from 'react-router';
import { TaskOutput } from '../../types';
type TaskPageLinksProps = {
output: TaskOutput;
};
export const TaskPageLinks = ({ output }: TaskPageLinksProps) => {
const { entityRef, remoteUrl } = output;
let { links = [] } = output;
const app = useApp();
const iconResolver = (key: IconKey | undefined): IconComponent =>
key ? app.getSystemIcon(key) ?? LanguageIcon : LanguageIcon;
if (remoteUrl) {
links = [{ url: remoteUrl, title: 'Repo' }, ...links];
}
if (entityRef) {
links = [
{
url: generatePath(
`/catalog/${entityRoute.path}`,
parseEntityName(entityRef),
),
title: 'Open in catalog',
icon: 'catalog',
},
...links,
];
}
return (
<Box px={3} pb={3}>
{links.map(({ url, title, icon }, i) => (
<IconLink
key={`output-link-${i}`}
href={url}
text={title ?? url}
Icon={iconResolver(icon)}
target="_blank"
/>
))}
</Box>
);
};
@@ -16,14 +16,9 @@
import { useImmerReducer } from 'use-immer';
import { useEffect } from 'react';
import { scaffolderApiRef, LogEvent } from '../../api';
import { ScaffolderTask, Status } from '../../types';
import { ScaffolderTask, Status, TaskOutput } from '../../types';
import { Subscription, useApi } from '@backstage/core';
type OutputLink = {
title: string;
url: string;
};
type Step = {
id: string;
status: Status;
@@ -31,11 +26,6 @@ type Step = {
startedAt?: string;
};
type TaskOutput = {
entityRef?: string;
links?: OutputLink[];
} & { [key in string]: string };
export type TaskStream = {
loading: boolean;
error?: Error;
+13
View File
@@ -64,3 +64,16 @@ export type ListActionsResponse = Array<{
output?: JSONSchema;
};
}>;
type OutputLink = {
url: string;
title?: string;
icon?: string;
};
export type TaskOutput = {
entityRef?: string;
remoteUrl?: string;
links?: OutputLink[];
[key: string]: any;
};