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 <heikki.hellgren@op.fi>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-shortcuts': minor
|
||||
---
|
||||
|
||||
Allow external links to be added as shortcuts
|
||||
@@ -167,7 +167,7 @@ export const Root = ({ children }: PropsWithChildren<{}>) => (
|
||||
<SidebarItem icon={Score} to="score-board" text="Score board" />
|
||||
</SidebarScrollWrapper>
|
||||
<SidebarDivider />
|
||||
<Shortcuts />
|
||||
<Shortcuts allowExternalLinks />
|
||||
</SidebarGroup>
|
||||
<SidebarSpace />
|
||||
<SidebarDivider />
|
||||
|
||||
@@ -44,6 +44,8 @@ export const SidebarComponent = () => (
|
||||
);
|
||||
```
|
||||
|
||||
To allow external links to be added as shortcut, you can add `allowExternalLinks` property to the `<Shortcuts />` 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`:
|
||||
|
||||
@@ -57,6 +57,8 @@ export const shortcutsPlugin: BackstagePlugin<{}, {}, {}>;
|
||||
|
||||
// @public
|
||||
export interface ShortcutsProps {
|
||||
// (undocumented)
|
||||
allowExternalLinks?: boolean;
|
||||
// (undocumented)
|
||||
icon?: IconComponent;
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</Card>
|
||||
</Popover>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</Card>
|
||||
</Popover>
|
||||
|
||||
@@ -43,6 +43,50 @@ describe('ShortcutForm', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('allows external links', async () => {
|
||||
await renderInTestApp(<ShortcutForm allowExternalLinks {...props} />);
|
||||
|
||||
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(<ShortcutForm allowExternalLinks {...props} />);
|
||||
|
||||
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(
|
||||
<ShortcutForm
|
||||
|
||||
@@ -40,11 +40,16 @@ type Props = {
|
||||
formValues?: FormValues;
|
||||
onSave: SubmitHandler<FormValues>;
|
||||
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 }) => (
|
||||
<TextField
|
||||
|
||||
@@ -58,9 +58,10 @@ const getIconText = (title: string) =>
|
||||
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<Element | undefined>();
|
||||
|
||||
@@ -100,6 +101,7 @@ export const ShortcutItem = ({ shortcut, api }: Props) => {
|
||||
anchorEl={anchorEl}
|
||||
api={api}
|
||||
shortcut={shortcut}
|
||||
allowExternalLinks={allowExternalLinks}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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 ? (
|
||||
<Progress />
|
||||
@@ -71,6 +73,7 @@ export const Shortcuts = (props: ShortcutsProps) => {
|
||||
key={shortcut.id}
|
||||
shortcut={shortcut}
|
||||
api={shortcutApi}
|
||||
allowExternalLinks={props.allowExternalLinks}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user