diff --git a/packages/catalog-model/src/schema/kinds/Template.v1beta2.schema.json b/packages/catalog-model/src/schema/kinds/Template.v1beta2.schema.json index bd9999f8bd..ba3ccb9d1d 100644 --- a/packages/catalog-model/src/schema/kinds/Template.v1beta2.schema.json +++ b/packages/catalog-model/src/schema/kinds/Template.v1beta2.schema.json @@ -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" } diff --git a/packages/core-api/src/icons/icons.tsx b/packages/core-api/src/icons/icons.tsx index e638131a28..fd3d0dcdab 100644 --- a/packages/core-api/src/icons/icons.tsx +++ b/packages/core-api/src/icons/icons.tsx @@ -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, diff --git a/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx b/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx index 96a857dbb5..cc1149ee61 100644 --- a/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx +++ b/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx @@ -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 (
{ currentStepId={currentStepId} onUserStepChange={setUserSelectedStepId} /> - {(entityRef || remoteUrl) && ( - - {entityRef && ( - - )} - {remoteUrl && ( - - )} - + {output && hasLinks(output) && ( + )} diff --git a/plugins/scaffolder/src/components/TaskPage/TaskPageLinks.tsx b/plugins/scaffolder/src/components/TaskPage/TaskPageLinks.tsx new file mode 100644 index 0000000000..931482194e --- /dev/null +++ b/plugins/scaffolder/src/components/TaskPage/TaskPageLinks.tsx @@ -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 ( + + {links.map(({ url, title, icon }, i) => ( + + ))} + + ); +}; diff --git a/plugins/scaffolder/src/components/hooks/useEventStream.ts b/plugins/scaffolder/src/components/hooks/useEventStream.ts index 958c3d192e..4d28fabd34 100644 --- a/plugins/scaffolder/src/components/hooks/useEventStream.ts +++ b/plugins/scaffolder/src/components/hooks/useEventStream.ts @@ -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; diff --git a/plugins/scaffolder/src/types.ts b/plugins/scaffolder/src/types.ts index 6a2c2eede4..f96d103796 100644 --- a/plugins/scaffolder/src/types.ts +++ b/plugins/scaffolder/src/types.ts @@ -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; +};