@@ -31,6 +31,7 @@
|
||||
"@backstage/plugin-scaffolder": "^0.9.0",
|
||||
"@backstage/plugin-search": "^0.3.4",
|
||||
"@backstage/plugin-sentry": "^0.3.8",
|
||||
"@backstage/plugin-shortcuts": "^0.1.1",
|
||||
"@backstage/plugin-tech-radar": "^0.3.9",
|
||||
"@backstage/plugin-techdocs": "^0.7.2",
|
||||
"@backstage/plugin-todo": "^0.1.0",
|
||||
|
||||
@@ -39,6 +39,7 @@ import { NavLink } from 'react-router-dom';
|
||||
import { GraphiQLIcon } from '@backstage/plugin-graphiql';
|
||||
import { Settings as SidebarSettings } from '@backstage/plugin-user-settings';
|
||||
import { SidebarSearch } from '@backstage/plugin-search';
|
||||
import { Shortcuts } from '@backstage/plugin-shortcuts';
|
||||
|
||||
const useSidebarLogoStyles = makeStyles({
|
||||
root: {
|
||||
@@ -91,6 +92,8 @@ export const Root = ({ children }: PropsWithChildren<{}>) => (
|
||||
<SidebarItem icon={RuleIcon} to="lighthouse" text="Lighthouse" />
|
||||
<SidebarItem icon={MoneyIcon} to="cost-insights" text="Cost Insights" />
|
||||
<SidebarItem icon={GraphiQLIcon} to="graphiql" text="GraphiQL" />
|
||||
<SidebarDivider />
|
||||
<Shortcuts />
|
||||
<SidebarSpace />
|
||||
<SidebarDivider />
|
||||
<SidebarSettings />
|
||||
|
||||
@@ -17,3 +17,5 @@
|
||||
// TODO(Rugvip): This plugin is currently not part of the app element tree,
|
||||
// ideally we have an API for the context menu that permits that.
|
||||
export { badgesPlugin } from '@backstage/plugin-badges';
|
||||
export { githubDeploymentsPlugin } from '@backstage/plugin-github-deployments';
|
||||
export { shortcutsPlugin } from '@backstage/plugin-shortcuts';
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
extends: [require.resolve('@backstage/cli/config/eslint')],
|
||||
};
|
||||
@@ -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.
|
||||
@@ -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();
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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) },
|
||||
}),
|
||||
);
|
||||
@@ -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';
|
||||
@@ -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;
|
||||
};
|
||||
@@ -18569,6 +18569,7 @@ minipass-fetch@^1.3.0, minipass-fetch@^1.3.2:
|
||||
resolved "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.3.3.tgz#34c7cea038c817a8658461bf35174551dce17a0a"
|
||||
integrity sha512-akCrLDWfbdAWkMLBxJEeWTdNsjML+dt5YgOI4gJ53vuO0vrmYQkUPxa6j6V65s9CcePIr2SSWqjT2EcrNseryQ==
|
||||
dependencies:
|
||||
encoding "^0.1.12"
|
||||
minipass "^3.1.0"
|
||||
minipass-sized "^1.0.3"
|
||||
minizlib "^2.0.0"
|
||||
@@ -21943,6 +21944,11 @@ react-hook-form@^6.15.4, react-hook-form@^6.6.0:
|
||||
resolved "https://registry.npmjs.org/react-hook-form/-/react-hook-form-6.15.4.tgz#328003e1ccc096cd158899ffe7e3b33735a9b024"
|
||||
integrity sha512-K+Sw33DtTMengs8OdqFJI3glzNl1wBzSefD/ksQw/hJf9CnOHQAU6qy82eOrh0IRNt2G53sjr7qnnw1JDjvx1w==
|
||||
|
||||
react-hook-form@^7.1.1:
|
||||
version "7.2.1"
|
||||
resolved "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.2.1.tgz#99b3540dd2314499df12e9a53c70587ad63a806c"
|
||||
integrity sha512-QopAubhVofqQrwlWLr9aK0DF8tNU8fnU8sJIlw1Tb3tGkEvP9yeaA+cx1hlxYni8xBswtHruL1WcDEa6CYQDow==
|
||||
|
||||
react-hot-loader@^4.12.21:
|
||||
version "4.13.0"
|
||||
resolved "https://registry.npmjs.org/react-hot-loader/-/react-hot-loader-4.13.0.tgz#c27e9408581c2a678f5316e69c061b226dc6a202"
|
||||
@@ -26095,7 +26101,7 @@ uuid@^7.0.3:
|
||||
resolved "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz#c5c9f2c8cf25dc0a372c4df1441c41f5bd0c680b"
|
||||
integrity sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==
|
||||
|
||||
uuid@^8.0.0, uuid@^8.2.0, uuid@^8.3.0:
|
||||
uuid@^8.0.0, uuid@^8.2.0, uuid@^8.3.0, uuid@^8.3.2:
|
||||
version "8.3.2"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
|
||||
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
|
||||
|
||||
Reference in New Issue
Block a user