From 8a1566719692d0d216807386e12bc89746b8cbe5 Mon Sep 17 00:00:00 2001 From: Andrew Thauer <6507159+andrewthauer@users.noreply.github.com> Date: Tue, 23 Feb 2021 15:47:55 -0500 Subject: [PATCH] feat(core): add support config & button/error components This adds a new app.support top level config that can be used to configure various support links and other information. This can later be extended for plugins to append their own specific support items. --- .changeset/silver-balloons-play.md | 5 + app-config.yaml | 13 ++ packages/core-api/src/app/App.tsx | 4 +- packages/core-api/src/app/types.ts | 4 +- packages/core-api/src/icons/icons.tsx | 27 +++- packages/core-api/src/icons/types.ts | 11 +- packages/core/config.d.ts | 35 +++++ .../SupportButton/SupportButton.tsx | 132 +++++++----------- packages/core/src/hooks/index.ts | 6 + packages/core/src/hooks/useSupportConfig.ts | 56 ++++++++ .../core/src/layout/ErrorPage/ErrorPage.tsx | 14 +- .../EntityLinksCard/EntityLinksCard.tsx | 5 +- .../DomainExplorerContent.test.tsx | 36 ++--- .../ToolExplorerContent.test.tsx | 32 +++-- .../ProfileCatalog/ProfileCatalog.test.tsx | 22 +-- .../RegisterComponentPage.test.tsx | 42 +++--- .../src/components/RadarPage.test.tsx | 29 ++-- .../WelcomePage/WelcomePage.test.tsx | 19 ++- 18 files changed, 301 insertions(+), 191 deletions(-) create mode 100644 .changeset/silver-balloons-play.md create mode 100644 packages/core/src/hooks/useSupportConfig.ts diff --git a/.changeset/silver-balloons-play.md b/.changeset/silver-balloons-play.md new file mode 100644 index 0000000000..24a5c8ac46 --- /dev/null +++ b/.changeset/silver-balloons-play.md @@ -0,0 +1,5 @@ +--- +'@backstage/core': patch +--- + +Added a new useSupportConfig hook that reads a new `app.support` config key. Also updated the SupportButton and ErrorPage components to use the new config. diff --git a/app-config.yaml b/app-config.yaml index 672ba26cd2..f68f5a30a2 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -2,6 +2,19 @@ app: title: Backstage Example App baseUrl: http://localhost:3000 googleAnalyticsTrackingId: # UA-000000-0 + support: + url: https://github.com/backstage/backstage/issues # Used by common ErrorPage + items: # Used by common SupportButton component + - title: Issues + icon: github + links: + - url: https://github.com/backstage/backstage/issues + title: GitHub Issues + - title: Discord Chatroom + icon: chat + links: + - url: https://discord.gg/MUpMjP2 + title: '#backstage' backend: baseUrl: http://localhost:7000 diff --git a/packages/core-api/src/app/App.tsx b/packages/core-api/src/app/App.tsx index 3cf5abdb44..1cf2d892d0 100644 --- a/packages/core-api/src/app/App.tsx +++ b/packages/core-api/src/app/App.tsx @@ -149,7 +149,7 @@ class AppContextImpl implements AppContext { return this.app.getPlugins(); } - getSystemIcon(key: string): IconComponent { + getSystemIcon(key: IconKey): IconComponent | undefined { return this.app.getSystemIcon(key); } @@ -206,7 +206,7 @@ export class PrivateAppImpl implements BackstageApp { return this.plugins; } - getSystemIcon(key: IconKey): IconComponent { + getSystemIcon(key: IconKey): IconComponent | undefined { return this.icons[key]; } diff --git a/packages/core-api/src/app/types.ts b/packages/core-api/src/app/types.ts index 69eb7f7189..ec7b70689d 100644 --- a/packages/core-api/src/app/types.ts +++ b/packages/core-api/src/app/types.ts @@ -171,7 +171,7 @@ export type BackstageApp = { /** * Get a common or custom icon for this app. */ - getSystemIcon(key: IconKey): IconComponent; + getSystemIcon(key: IconKey): IconComponent | undefined; /** * Provider component that should wrap the Router created with getRouter() @@ -202,7 +202,7 @@ export type AppContext = { /** * Get a common or custom icon for this app. */ - getSystemIcon(key: IconKey): IconComponent; + getSystemIcon(key: IconKey): IconComponent | undefined; /** * Get the components registered for various purposes in the app. diff --git a/packages/core-api/src/icons/icons.tsx b/packages/core-api/src/icons/icons.tsx index 0c2b580ea7..49db90efcc 100644 --- a/packages/core-api/src/icons/icons.tsx +++ b/packages/core-api/src/icons/icons.tsx @@ -15,31 +15,46 @@ */ import { SvgIconProps } from '@material-ui/core'; +import MuiBrokenImageIcon from '@material-ui/icons/BrokenImage'; +import MuiChatIcon from '@material-ui/icons/Chat'; import MuiDashboardIcon from '@material-ui/icons/Dashboard'; +import MuiEmailIcon from '@material-ui/icons/Email'; +import MuiGitHubIcon from '@material-ui/icons/GitHub'; import MuiHelpIcon from '@material-ui/icons/Help'; -import PeopleIcon from '@material-ui/icons/People'; -import PersonIcon from '@material-ui/icons/Person'; +import MuiPeopleIcon from '@material-ui/icons/People'; +import MuiPersonIcon from '@material-ui/icons/Person'; +import MuiWarningIcon from '@material-ui/icons/Warning'; import React from 'react'; import { useApp } from '../app/AppContext'; -import { IconComponent, SystemIconKey, IconComponentMap } from './types'; +import { IconComponent, IconComponentMap, SystemIconKey } from './types'; export const defaultSystemIcons: IconComponentMap = { - user: PersonIcon, - group: PeopleIcon, + brokenImage: MuiBrokenImageIcon, + chat: MuiChatIcon, dashboard: MuiDashboardIcon, + email: MuiEmailIcon, + github: MuiGitHubIcon, + group: MuiPeopleIcon, help: MuiHelpIcon, + user: MuiPersonIcon, + warning: MuiWarningIcon, }; const overridableSystemIcon = (key: SystemIconKey): IconComponent => { const Component = (props: SvgIconProps) => { const app = useApp(); const Icon = app.getSystemIcon(key); - return ; + return Icon ? : ; }; return Component; }; +export const BrokenImageIcon = overridableSystemIcon('brokenImage'); +export const ChatIcon = overridableSystemIcon('chat'); export const DashboardIcon = overridableSystemIcon('dashboard'); +export const EmailIcon = overridableSystemIcon('email'); +export const GitHubIcon = overridableSystemIcon('github'); export const GroupIcon = overridableSystemIcon('group'); export const HelpIcon = overridableSystemIcon('help'); export const UserIcon = overridableSystemIcon('user'); +export const WarningIcon = overridableSystemIcon('warning'); diff --git a/packages/core-api/src/icons/types.ts b/packages/core-api/src/icons/types.ts index be24b223ae..c4634bd00a 100644 --- a/packages/core-api/src/icons/types.ts +++ b/packages/core-api/src/icons/types.ts @@ -17,7 +17,16 @@ import { ComponentType } from 'react'; import { SvgIconProps } from '@material-ui/core'; -export type SystemIconKey = 'user' | 'group' | 'dashboard' | 'help'; +export type SystemIconKey = + | 'brokenImage' + | 'chat' + | 'dashboard' + | 'email' + | 'github' + | 'group' + | 'help' + | 'user' + | 'warning'; export type IconComponent = ComponentType; export type IconKey = SystemIconKey | string; diff --git a/packages/core/config.d.ts b/packages/core/config.d.ts index a6f95ae71c..bc6c462014 100644 --- a/packages/core/config.d.ts +++ b/packages/core/config.d.ts @@ -30,6 +30,41 @@ export interface Config { * @visibility frontend */ title?: string; + + /** + * Information about support of this Backstage instance and how to contact the integrator team. + */ + support?: { + /** + * The primary support url. + * @visibility frontend + */ + url: string; + /** + * A list of categorized support item groupings. + */ + items: { + /** + * The title of the support item grouping. + * @visibility frontend + */ + title: string; + /** + * An optional icon for the support item grouping. + * @visibility frontend + */ + icon?: string; + /** + * A list of support links for the Backstage instance. + */ + links?: { + /** @visibility frontend */ + url: string; + /** @visibility frontend */ + title?: string; + }[]; + }[]; + }; }; /** diff --git a/packages/core/src/components/SupportButton/SupportButton.tsx b/packages/core/src/components/SupportButton/SupportButton.tsx index 2a1b810582..7c532c0f8f 100644 --- a/packages/core/src/components/SupportButton/SupportButton.tsx +++ b/packages/core/src/components/SupportButton/SupportButton.tsx @@ -14,35 +14,26 @@ * limitations under the License. */ -import React, { - Fragment, - useState, - MouseEventHandler, - PropsWithChildren, -} from 'react'; +import { HelpIcon, useApp } from '@backstage/core-api'; import { Button, - Link, List, ListItem, ListItemIcon, - Popover, - Typography, - makeStyles, ListItemText, + makeStyles, + Popover, } from '@material-ui/core'; -import GroupIcon from '@material-ui/icons/Group'; -import HelpIcon from '@material-ui/icons/Help'; +import React, { + Fragment, + MouseEventHandler, + PropsWithChildren, + useState, +} from 'react'; +import { SupportItem, SupportItemLink, useSupportConfig } from '../../hooks'; +import { Link } from '../Link'; -// import { EmailIcon, SlackIcon, SupportIcon } from 'shared/icons'; -// import { Button, Link } from 'shared/components'; -// import { StackOverflow, StackOverflowTag } from 'shared/components/layout'; - -type Props = { - slackChannel?: string | string[]; - email?: string | string[]; - plugin?: any; -}; +type Props = {}; const useStyles = makeStyles(theme => ({ leftIcon: { @@ -50,17 +41,45 @@ const useStyles = makeStyles(theme => ({ }, popoverList: { minWidth: 260, - maxWidth: 320, + maxWidth: 400, }, })); -export const SupportButton = ({ - slackChannel = '#backstage', - email = [], - children, -}: // plugin, -PropsWithChildren) => { - // TODO: get plugin manifest with hook +const SupportIcon = ({ icon }: { icon: string | undefined }) => { + const app = useApp(); + const Icon = icon ? app.getSystemIcon(icon) ?? HelpIcon : HelpIcon; + return ; +}; + +const SupportLink = ({ link }: { link: SupportItemLink }) => ( + + {link.title ?? link.url} + +); + +const SupportListItem = ({ item }: { item: SupportItem }) => { + return ( + + + + + + {item.links && + item.links.map(link => ( + + ))} + + } + /> + + ); +}; + +export const SupportButton = ({ children }: PropsWithChildren) => { + const { items } = useSupportConfig(); const [popoverOpen, setPopoverOpen] = useState(false); const [anchorEl, setAnchorEl] = useState(null); @@ -75,12 +94,6 @@ PropsWithChildren) => { setPopoverOpen(false); }; - // const tags = plugin ? plugin.stackoverflowTags : undefined; - const slackChannels = Array.isArray(slackChannel) - ? slackChannel - : [slackChannel]; - const contactEmails = Array.isArray(email) ? email : [email]; - return (