Add new shortcut plugin

Signed-off-by: Marcus Eide <eide@spotify.com>
This commit is contained in:
Marcus Eide
2021-04-20 11:21:38 +02:00
parent 4e7c9d7d16
commit 7812b6ac3c
22 changed files with 1010 additions and 1 deletions
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint')],
};
+13
View File
@@ -0,0 +1,13 @@
# shortcuts
Welcome to the shortcuts plugin!
_This plugin was created through the Backstage CLI_
## Getting started
Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/shortcuts](http://localhost:3000/shortcuts).
You can also serve the plugin in isolation by running `yarn start` in the plugin directory.
This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads.
It is only meant for local development, and the setup for it can be found inside the [/dev](./dev) directory.
+26
View File
@@ -0,0 +1,26 @@
/*
* 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 { createDevApp } from '@backstage/dev-utils';
import { shortcutsPlugin, Shortcuts } from '../src/plugin';
createDevApp()
.registerPlugin(shortcutsPlugin)
.addPage({
element: <Shortcuts />,
title: 'Root Page',
})
.render();
+51
View File
@@ -0,0 +1,51 @@
{
"name": "@backstage/plugin-shortcuts",
"version": "0.1.1",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
"types": "dist/index.d.ts"
},
"scripts": {
"build": "backstage-cli plugin:build",
"start": "backstage-cli plugin:serve",
"lint": "backstage-cli lint",
"test": "backstage-cli test",
"diff": "backstage-cli plugin:diff",
"prepack": "backstage-cli prepack",
"postpack": "backstage-cli postpack",
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/core": "^0.7.5",
"@backstage/theme": "^0.2.5",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-hook-form": "^7.1.1",
"react-router": "6.0.0-beta.0",
"react-use": "^15.3.3",
"uuid": "^8.3.2",
"zen-observable": "^0.8.15"
},
"devDependencies": {
"@backstage/cli": "^0.6.8",
"@backstage/dev-utils": "^0.1.13",
"@backstage/test-utils": "^0.1.10",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^11.2.5",
"@testing-library/user-event": "^12.0.7",
"@types/jest": "^26.0.7",
"@types/node": "^14.14.32",
"cross-fetch": "^3.0.6",
"msw": "^0.21.2"
},
"files": [
"dist"
]
}
+121
View File
@@ -0,0 +1,121 @@
/*
* 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, { useState } from 'react';
import { useLocation } from 'react-router';
import { SubmitHandler } from 'react-hook-form';
import { alertApiRef, useApi } from '@backstage/core';
import {
Button,
Card,
CardHeader,
makeStyles,
Popover,
} from '@material-ui/core';
import { ShortcutForm } from './ShortcutForm';
import { FormValues, Shortcut } from './types';
import { ShortcutApi } from './api';
const useStyles = makeStyles(theme => ({
card: {
width: 400,
},
header: {
marginBottom: theme.spacing(1),
},
button: {
marginTop: theme.spacing(1),
},
}));
type Props = {
onClose: () => void;
anchorEl?: Element;
api: ShortcutApi;
};
export const AddShortcut = ({ onClose, anchorEl, api }: Props) => {
const classes = useStyles();
const alertApi = useApi(alertApiRef);
const { pathname } = useLocation();
const [formValues, setFormValues] = useState<FormValues>();
const open = Boolean(anchorEl);
const handleSave: SubmitHandler<FormValues> = async ({ url, title }) => {
const shortcut: Omit<Shortcut, 'id'> = { url, title };
try {
await api.add(shortcut);
alertApi.post({
message: 'Successfully added shortcut',
severity: 'success',
});
} catch (error) {
alertApi.post({
message: `Could not add shortcut: ${error.message}`,
severity: 'error',
});
}
onClose();
};
const handlePaste = () => {
setFormValues({ url: pathname, title: document.title });
};
const handleClose = () => {
setFormValues(undefined);
onClose();
};
return (
<Popover
open={open}
anchorEl={anchorEl}
onExit={handleClose}
onClose={onClose}
anchorOrigin={{
vertical: 'top',
horizontal: 'right',
}}
>
<Card className={classes.card}>
<CardHeader
className={classes.header}
title="Add Shortcut"
titleTypographyProps={{ variant: 'subtitle2' }}
action={
<Button
className={classes.button}
variant="text"
size="small"
color="primary"
onClick={handlePaste}
>
Paste Current Url
</Button>
}
/>
<ShortcutForm
onClose={handleClose}
onSave={handleSave}
formValues={formValues}
/>
</Card>
</Popover>
);
};
+134
View File
@@ -0,0 +1,134 @@
/*
* 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 { SubmitHandler } from 'react-hook-form';
import { alertApiRef, useApi } from '@backstage/core';
import {
Button,
Card,
CardHeader,
makeStyles,
Popover,
} from '@material-ui/core';
import { ShortcutForm } from './ShortcutForm';
import { FormValues, Shortcut } from './types';
import DeleteIcon from '@material-ui/icons/Delete';
import { ShortcutApi } from './api';
const useStyles = makeStyles(theme => ({
card: {
width: 400,
},
header: {
marginBottom: theme.spacing(1),
},
button: {
marginTop: theme.spacing(1),
},
}));
type Props = {
shortcut: Shortcut;
onClose: () => void;
anchorEl?: Element;
api: ShortcutApi;
};
export const EditShortcut = ({ shortcut, onClose, anchorEl, api }: Props) => {
const classes = useStyles();
const alertApi = useApi(alertApiRef);
const open = Boolean(anchorEl);
const handleSave: SubmitHandler<FormValues> = async ({ url, title }) => {
const newShortcut: Shortcut = {
...shortcut,
url,
title,
};
try {
await api.update(newShortcut);
alertApi.post({
message: 'Successfully updated shortcut',
severity: 'success',
});
} catch (error) {
alertApi.post({
message: `Could not update shortcut: ${error.message}`,
severity: 'error',
});
}
onClose();
};
const handleRemove = async () => {
try {
await api.remove(shortcut);
alertApi.post({
message: 'Successfully deleted shortcut',
severity: 'success',
});
} catch (error) {
alertApi.post({
message: `Could not delete shortcut: ${error.message}`,
severity: 'error',
});
}
};
const handleClose = () => {
onClose();
};
return (
<Popover
open={open}
anchorEl={anchorEl}
onClose={onClose}
anchorOrigin={{
vertical: 'top',
horizontal: 'right',
}}
>
<Card className={classes.card}>
<CardHeader
className={classes.header}
title="Edit Shortcut"
titleTypographyProps={{ variant: 'subtitle2' }}
action={
<Button
className={classes.button}
variant="text"
size="small"
color="secondary"
startIcon={<DeleteIcon />}
onClick={handleRemove}
>
Remove
</Button>
}
/>
<ShortcutForm
formValues={{ url: shortcut.url, title: shortcut.title }}
onClose={handleClose}
onSave={handleSave}
/>
</Card>
</Popover>
);
};
+140
View File
@@ -0,0 +1,140 @@
/*
* 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, { useEffect } from 'react';
import { useForm, SubmitHandler, Controller } from 'react-hook-form';
import {
Button,
CardActions,
CardContent,
makeStyles,
TextField,
} from '@material-ui/core';
import { FormValues } from './types';
const useStyles = makeStyles(theme => ({
field: {
marginBottom: theme.spacing(2),
},
actionRoot: {
paddingLeft: theme.spacing(2),
paddingBottom: theme.spacing(3),
justifyContent: 'flex-start',
},
}));
type Props = {
formValues?: FormValues;
onSave: SubmitHandler<FormValues>;
onClose: () => void;
};
export const ShortcutForm = ({ formValues, onSave, onClose }: Props) => {
const classes = useStyles();
const {
handleSubmit,
reset,
control,
formState: { errors },
} = useForm<FormValues>({
mode: 'onChange',
defaultValues: {
url: formValues?.url ?? '',
title: formValues?.title ?? '',
},
});
useEffect(() => {
reset(formValues);
}, [reset, formValues]);
return (
<>
<CardContent>
<Controller
name="url"
control={control}
rules={{
required: true,
pattern: {
value: /^\//,
message: 'Must be a relative URL (starts with a /)',
},
}}
render={({ field }) => (
<TextField
{...field}
error={!!errors.url}
helperText={errors.url?.message}
type="text"
placeholder="Enter a URL"
InputLabelProps={{
shrink: true,
}}
className={classes.field}
fullWidth
label="Shortcut URL"
variant="outlined"
autoComplete="off"
/>
)}
/>
<Controller
name="title"
control={control}
rules={{
required: true,
minLength: {
value: 2,
message: 'Must be at least 2 characters',
},
}}
render={({ field }) => (
<TextField
{...field}
error={!!errors.title}
helperText={errors.title?.message}
type="text"
placeholder="Enter a display name"
InputLabelProps={{
shrink: true,
}}
className={classes.field}
fullWidth
label="Display Name"
variant="outlined"
autoComplete="off"
/>
)}
/>
</CardContent>
<CardActions classes={{ root: classes.actionRoot }}>
<Button
variant="contained"
color="primary"
size="large"
onClick={handleSubmit(onSave)}
>
Save
</Button>
<Button variant="outlined" size="large" onClick={onClose}>
Cancel
</Button>
</CardActions>
</>
);
};
+47
View File
@@ -0,0 +1,47 @@
/*
* 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';
type Props = {
text: string;
color: string;
};
export const ShortcutIcon = ({ text, color }: Props) => {
const size = 28;
return (
<svg
viewBox={`0 0 ${size} ${size}`}
height={size}
width={size}
style={{ filter: 'contrast(150%) brightness(1.4)' }}
>
<circle r={size / 2} cx={size / 2} cy={size / 2} fill={color} />
<text
dy={2 + size / 2}
dx={size / 2}
fontWeight="bold"
fontSize="13"
textAnchor="middle"
alignmentBaseline="middle"
fill="black"
>
{text}
</text>
</svg>
);
};
+98
View File
@@ -0,0 +1,98 @@
/*
* 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, { useState } from 'react';
import { SidebarItem } from '@backstage/core';
import { IconButton, makeStyles } from '@material-ui/core';
import EditIcon from '@material-ui/icons/Edit';
import { ShortcutIcon } from './ShortcutIcon';
import { EditShortcut } from './EditShortcut';
import { ShortcutApi } from './api';
import { Shortcut } from './types';
const useStyles = makeStyles({
icon: {
color: 'white',
fontSize: 16,
},
});
const getIconText = (title: string) =>
title.split(' ').length === 1
? // If there's only one word, keep the first two characters
title[0].toUpperCase() + title[1].toLowerCase()
: // If there's more than one word, take the first character of the first two words
title
.replace(/\W+/g, ' ')
.split(' ')
.map(s => s[0])
.join('')
.slice(0, 2)
.toUpperCase();
type Props = {
shortcut: Shortcut;
api: ShortcutApi;
};
export const ShortcutItem = ({ shortcut, api }: Props) => {
const classes = useStyles();
const [displayEdit, setDisplayEdit] = useState(false);
const [anchorEl, setAnchorEl] = React.useState<Element | undefined>();
const handleClick = (event: React.MouseEvent<Element>) => {
event.preventDefault();
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(undefined);
setDisplayEdit(false);
};
const handleMouseEnter = () => {
setDisplayEdit(true);
};
const handleMouseLeave = () => {
setDisplayEdit(false);
};
const text = getIconText(shortcut.title);
const color = api.getColor(shortcut.url);
return (
<div onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
<SidebarItem
to={shortcut.url}
text={shortcut.title}
icon={() => <ShortcutIcon text={text} color={color} />}
>
{displayEdit && (
<IconButton data-testid="edit" onClick={handleClick}>
<EditIcon className={classes.icon} />
</IconButton>
)}
</SidebarItem>
<EditShortcut
onClose={handleClose}
anchorEl={anchorEl}
api={api}
shortcut={shortcut}
/>
</div>
);
};
+66
View File
@@ -0,0 +1,66 @@
/*
* 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, { useMemo } from 'react';
import { useObservable } from 'react-use';
import { Progress, SidebarItem, useApi } from '@backstage/core';
import PlayListAddIcon from '@material-ui/icons/PlaylistAdd';
import { ShortcutItem } from './ShortcutItem';
import { AddShortcut } from './AddShortcut';
import { shortcutsApiRef } from './api';
export const Shortcuts = () => {
const shortcutApi = useApi(shortcutsApiRef);
const shortcuts = useObservable(
useMemo(() => shortcutApi.observe(), [shortcutApi]),
);
const [anchorEl, setAnchorEl] = React.useState<Element | undefined>();
const loading = Boolean(!shortcuts);
const handleClick = (event: React.MouseEvent<Element>) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(undefined);
};
return (
<>
<SidebarItem
icon={PlayListAddIcon}
text="Add Shortcuts"
onClick={handleClick}
/>
<AddShortcut
onClose={handleClose}
anchorEl={anchorEl}
api={shortcutApi}
/>
{loading ? (
<Progress />
) : (
shortcuts?.map(shortcut => (
<ShortcutItem
key={shortcut.id}
shortcut={shortcut}
api={shortcutApi}
/>
))
)}
</>
);
};
@@ -0,0 +1,97 @@
/*
* 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 { StorageApi } from '@backstage/core';
import { pageTheme } from '@backstage/theme';
import ObservableImpl from 'zen-observable';
import { v4 as uuid } from 'uuid';
import { ShortcutApi } from './ShortcutApi';
import { Shortcut } from '../types';
/**
* Implementation of the ShortcutApi that uses the StorageApi to store shortcuts.
*/
export class LocalStoredShortcuts implements ShortcutApi {
constructor(private readonly storageApi: StorageApi) {}
observe() {
return this.observable;
}
async add(shortcut: Omit<Shortcut, 'id'>) {
const shortcuts = this.get();
shortcuts.push({ ...shortcut, id: uuid() });
await this.storageApi.set('items', shortcuts);
this.notify();
}
async remove(shortcut: Shortcut) {
const shortcuts = this.get().filter(s => s.id !== shortcut.id);
await this.storageApi.set('items', shortcuts);
this.notify();
}
async update(shortcut: Shortcut) {
const shortcuts = this.get().filter(s => s.id !== shortcut.id);
shortcuts.push(shortcut);
await this.storageApi.set('items', shortcuts);
this.notify();
}
getColor(url: string) {
const type = url.split('/')[1];
const theme =
this.THEME_MAP[type] ??
(Object.keys(pageTheme).includes(type) ? type : 'tool');
return pageTheme[theme].colors[0];
}
private subscribers = new Set<
ZenObservable.SubscriptionObserver<Shortcut[]>
>();
private readonly observable = new ObservableImpl<Shortcut[]>(subscriber => {
subscriber.next(this.get());
this.subscribers.add(subscriber);
return () => {
this.subscribers.delete(subscriber);
};
});
private readonly THEME_MAP: Record<string, keyof typeof pageTheme> = {
catalog: 'home',
docs: 'documentation',
};
private get() {
return (
(this.storageApi.get('items') as Shortcut[])?.sort((a, b) =>
a.id >= b.id ? 1 : -1,
) ?? []
);
}
private notify() {
for (const subscription of this.subscribers) {
subscription.next(this.get());
}
}
}
+55
View File
@@ -0,0 +1,55 @@
/*
* 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 Observable from 'zen-observable';
import { createApiRef } from '@backstage/core';
import { Shortcut } from '../types';
export const shortcutsApiRef = createApiRef<ShortcutApi>({
id: 'plugin.shortcuts.api',
description: 'API to handle shortcuts in a Backstage Sidebar',
});
export interface ShortcutApi {
/**
* Returns an Observable that will subscribe to changes.
*/
observe(): Observable<Shortcut[]>;
/**
* Generates a unique id for the shortcut and then saves it.
*/
add(shortcut: Omit<Shortcut, 'id'>): Promise<void>;
/**
* Removes the shortcut.
*/
remove(shortcut: Shortcut): Promise<void>;
/**
* Finds an existing shortcut that matches the ID of the
* supplied shortcut and updates its values.
*/
update(shortcut: Shortcut): Promise<void>;
/**
* Each shortcut should get a color for its icon based on the url.
*
* Preferably using some abstraction between the url and the actual
* color value.
*/
getColor(url: string): string;
}
+19
View File
@@ -0,0 +1,19 @@
/*
* 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.
*/
export { LocalStoredShortcuts } from './LocalStoredShortcuts';
export { shortcutsApiRef } from './ShortcutApi';
export type { ShortcutApi } from './ShortcutApi';
+18
View File
@@ -0,0 +1,18 @@
/*
* 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.
*/
export { shortcutsPlugin, Shortcuts } from './plugin';
export * from './api';
export * from './types';
+22
View File
@@ -0,0 +1,22 @@
/*
* 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 { shortcutsPlugin } from './plugin';
describe('shortcuts', () => {
it('should export plugin', () => {
expect(shortcutsPlugin).toBeDefined();
});
});
+44
View File
@@ -0,0 +1,44 @@
/*
* 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 {
createApiFactory,
createComponentExtension,
createPlugin,
errorApiRef,
WebStorage,
} from '@backstage/core';
import { shortcutsApiRef, LocalStoredShortcuts } from './api';
export const shortcutsPlugin = createPlugin({
id: 'shortcuts',
apis: [
createApiFactory({
api: shortcutsApiRef,
deps: { errorApi: errorApiRef },
factory: ({ errorApi }) =>
new LocalStoredShortcuts(
WebStorage.create({ namespace: '@backstage/shortcuts', errorApi }),
),
}),
],
});
export const Shortcuts = shortcutsPlugin.provide(
createComponentExtension({
component: { lazy: () => import('./Shortcuts').then(m => m.Shortcuts) },
}),
);
+17
View File
@@ -0,0 +1,17 @@
/*
* 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 '@testing-library/jest-dom';
import 'cross-fetch/polyfill';
+26
View File
@@ -0,0 +1,26 @@
/*
* 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.
*/
export type Shortcut = {
id: string;
url: string;
title: string;
};
export type FormValues = {
url: string;
title: string;
};