diff --git a/.changeset/strong-crews-repeat.md b/.changeset/strong-crews-repeat.md
new file mode 100644
index 0000000000..1e772ae036
--- /dev/null
+++ b/.changeset/strong-crews-repeat.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-shortcuts': patch
+---
+
+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 19c0035a55..0dd42f5344 100644
--- a/plugins/shortcuts/api-report.md
+++ b/plugins/shortcuts/api-report.md
@@ -58,6 +58,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}
/>
))
)}