From 99df676e3241618bf162267edd8e01d95b5ff673 Mon Sep 17 00:00:00 2001 From: Heikki Hellgren Date: Thu, 9 Mar 2023 17:06:56 +0200 Subject: [PATCH 1/2] feat: allow adding external links to shortcuts This is behind a new property `allowExternalLinks` to make sure the application developer knows what he is doing. Only http(s) links are allowed. Signed-off-by: Heikki Hellgren --- .changeset/strong-crews-repeat.md | 5 +++ packages/app/src/components/Root/Root.tsx | 2 +- plugins/shortcuts/README.md | 2 + plugins/shortcuts/api-report.md | 2 + plugins/shortcuts/src/AddShortcut.tsx | 10 ++++- plugins/shortcuts/src/EditShortcut.tsx | 10 ++++- plugins/shortcuts/src/ShortcutForm.test.tsx | 44 +++++++++++++++++++++ plugins/shortcuts/src/ShortcutForm.tsx | 26 +++++++++--- plugins/shortcuts/src/ShortcutItem.tsx | 4 +- plugins/shortcuts/src/Shortcuts.tsx | 3 ++ 10 files changed, 98 insertions(+), 10 deletions(-) create mode 100644 .changeset/strong-crews-repeat.md diff --git a/.changeset/strong-crews-repeat.md b/.changeset/strong-crews-repeat.md new file mode 100644 index 0000000000..144c1842c6 --- /dev/null +++ b/.changeset/strong-crews-repeat.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-shortcuts': minor +--- + +Allow external links to be added as shortcuts diff --git a/packages/app/src/components/Root/Root.tsx b/packages/app/src/components/Root/Root.tsx index 8cec54326a..8b71637773 100644 --- a/packages/app/src/components/Root/Root.tsx +++ b/packages/app/src/components/Root/Root.tsx @@ -167,7 +167,7 @@ export const Root = ({ children }: PropsWithChildren<{}>) => ( - + diff --git a/plugins/shortcuts/README.md b/plugins/shortcuts/README.md index cd79901656..46b878a579 100644 --- a/plugins/shortcuts/README.md +++ b/plugins/shortcuts/README.md @@ -44,6 +44,8 @@ export const SidebarComponent = () => ( ); ``` +To allow external links to be added as shortcut, you can add `allowExternalLinks` property to the `` component. + The plugin exports a `shortcutApiRef` but the plugin includes a default implementation of the `ShortcutApi` that uses `localStorage` to store each user's shortcuts. To overwrite the default implementation add it to the App's `apis.ts`: diff --git a/plugins/shortcuts/api-report.md b/plugins/shortcuts/api-report.md index 1aa1dea003..657db8e2ca 100644 --- a/plugins/shortcuts/api-report.md +++ b/plugins/shortcuts/api-report.md @@ -57,6 +57,8 @@ export const shortcutsPlugin: BackstagePlugin<{}, {}, {}>; // @public export interface ShortcutsProps { + // (undocumented) + allowExternalLinks?: boolean; // (undocumented) icon?: IconComponent; } diff --git a/plugins/shortcuts/src/AddShortcut.tsx b/plugins/shortcuts/src/AddShortcut.tsx index e4b1ba84ce..1e14767531 100644 --- a/plugins/shortcuts/src/AddShortcut.tsx +++ b/plugins/shortcuts/src/AddShortcut.tsx @@ -45,9 +45,16 @@ type Props = { onClose: () => void; anchorEl?: Element; api: ShortcutApi; + + allowExternalLinks?: boolean; }; -export const AddShortcut = ({ onClose, anchorEl, api }: Props) => { +export const AddShortcut = ({ + onClose, + anchorEl, + api, + allowExternalLinks, +}: Props) => { const classes = useStyles(); const alertApi = useApi(alertApiRef); const { pathname } = useLocation(); @@ -114,6 +121,7 @@ export const AddShortcut = ({ onClose, anchorEl, api }: Props) => { onClose={handleClose} onSave={handleSave} formValues={formValues} + allowExternalLinks={allowExternalLinks} /> diff --git a/plugins/shortcuts/src/EditShortcut.tsx b/plugins/shortcuts/src/EditShortcut.tsx index 702275e90e..5336841633 100644 --- a/plugins/shortcuts/src/EditShortcut.tsx +++ b/plugins/shortcuts/src/EditShortcut.tsx @@ -46,9 +46,16 @@ type Props = { onClose: () => void; anchorEl?: Element; api: ShortcutApi; + allowExternalLinks?: boolean; }; -export const EditShortcut = ({ shortcut, onClose, anchorEl, api }: Props) => { +export const EditShortcut = ({ + shortcut, + onClose, + anchorEl, + api, + allowExternalLinks, +}: Props) => { const classes = useStyles(); const alertApi = useApi(alertApiRef); const open = Boolean(anchorEl); @@ -127,6 +134,7 @@ export const EditShortcut = ({ shortcut, onClose, anchorEl, api }: Props) => { formValues={{ url: shortcut.url, title: shortcut.title }} onClose={handleClose} onSave={handleSave} + allowExternalLinks={allowExternalLinks} /> diff --git a/plugins/shortcuts/src/ShortcutForm.test.tsx b/plugins/shortcuts/src/ShortcutForm.test.tsx index 2582148fc7..3a52356b5d 100644 --- a/plugins/shortcuts/src/ShortcutForm.test.tsx +++ b/plugins/shortcuts/src/ShortcutForm.test.tsx @@ -43,6 +43,50 @@ describe('ShortcutForm', () => { }); }); + it('allows external links', async () => { + await renderInTestApp(); + + const urlInput = screen.getByPlaceholderText('Enter a URL'); + const titleInput = screen.getByPlaceholderText('Enter a display name'); + fireEvent.change(urlInput, { + target: { value: 'https://www.backstage.io' }, + }); + fireEvent.change(titleInput, { target: { value: 'Backstage' } }); + + fireEvent.click(screen.getByText('Save')); + await waitFor(() => { + expect(props.onSave).toHaveBeenCalledWith( + expect.objectContaining({ + title: 'Backstage', + url: 'https://www.backstage.io', + }), + expect.anything(), + ); + }); + }); + + it('allows relative links when external links are enabled', async () => { + await renderInTestApp(); + + const urlInput = screen.getByPlaceholderText('Enter a URL'); + const titleInput = screen.getByPlaceholderText('Enter a display name'); + fireEvent.change(urlInput, { + target: { value: '/catalog' }, + }); + fireEvent.change(titleInput, { target: { value: 'Catalog' } }); + + fireEvent.click(screen.getByText('Save')); + await waitFor(() => { + expect(props.onSave).toHaveBeenCalledWith( + expect.objectContaining({ + title: 'Catalog', + url: '/catalog', + }), + expect.anything(), + ); + }); + }); + it('calls the save handler', async () => { await renderInTestApp( ; onClose: () => void; + allowExternalLinks?: boolean; }; -export const ShortcutForm = ({ formValues, onSave, onClose }: Props) => { +export const ShortcutForm = ({ + formValues, + onSave, + onClose, + allowExternalLinks, +}: Props) => { const classes = useStyles(); - const { handleSubmit, reset, @@ -70,10 +75,19 @@ export const ShortcutForm = ({ formValues, onSave, onClose }: Props) => { control={control} rules={{ required: true, - pattern: { - value: /^\//, - message: 'Must be a relative URL (starts with a /)', - }, + ...(allowExternalLinks + ? { + pattern: { + value: /^(https?:\/\/)|\//, + message: 'Must start with http(s):// or /', + }, + } + : { + pattern: { + value: /^\//, + message: 'Must be a relative URL (starts with a /)', + }, + }), }} render={({ field }) => ( type Props = { shortcut: Shortcut; api: ShortcutApi; + allowExternalLinks?: boolean; }; -export const ShortcutItem = ({ shortcut, api }: Props) => { +export const ShortcutItem = ({ shortcut, api, allowExternalLinks }: Props) => { const classes = useStyles(); const [anchorEl, setAnchorEl] = React.useState(); @@ -100,6 +101,7 @@ export const ShortcutItem = ({ shortcut, api }: Props) => { anchorEl={anchorEl} api={api} shortcut={shortcut} + allowExternalLinks={allowExternalLinks} /> ); diff --git a/plugins/shortcuts/src/Shortcuts.tsx b/plugins/shortcuts/src/Shortcuts.tsx index 28aa33e8f2..6b9ea4d79b 100644 --- a/plugins/shortcuts/src/Shortcuts.tsx +++ b/plugins/shortcuts/src/Shortcuts.tsx @@ -35,6 +35,7 @@ import { IconComponent, useApi } from '@backstage/core-plugin-api'; */ export interface ShortcutsProps { icon?: IconComponent; + allowExternalLinks?: boolean; } export const Shortcuts = (props: ShortcutsProps) => { @@ -62,6 +63,7 @@ export const Shortcuts = (props: ShortcutsProps) => { onClose={handleClose} anchorEl={anchorEl} api={shortcutApi} + allowExternalLinks={props.allowExternalLinks} /> {loading ? ( @@ -71,6 +73,7 @@ export const Shortcuts = (props: ShortcutsProps) => { key={shortcut.id} shortcut={shortcut} api={shortcutApi} + allowExternalLinks={props.allowExternalLinks} /> )) )} From 46fe170c05366378f5b25ea016c18aab28a9e65c Mon Sep 17 00:00:00 2001 From: drodil Date: Mon, 13 Mar 2023 13:16:53 +0200 Subject: [PATCH 2/2] Update .changeset/strong-crews-repeat.md Co-authored-by: Patrik Oldsberg Signed-off-by: drodil --- .changeset/strong-crews-repeat.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/strong-crews-repeat.md b/.changeset/strong-crews-repeat.md index 144c1842c6..1e772ae036 100644 --- a/.changeset/strong-crews-repeat.md +++ b/.changeset/strong-crews-repeat.md @@ -1,5 +1,5 @@ --- -'@backstage/plugin-shortcuts': minor +'@backstage/plugin-shortcuts': patch --- Allow external links to be added as shortcuts