From 7812b6ac3cfdd3964c23c0c225ac762c91e7c622 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Tue, 20 Apr 2021 11:21:38 +0200 Subject: [PATCH 01/27] Add new shortcut plugin Signed-off-by: Marcus Eide --- packages/app/package.json | 1 + packages/app/src/components/Root/Root.tsx | 3 + packages/app/src/plugins.ts | 2 + plugins/shortcuts/.eslintrc.js | 3 + plugins/shortcuts/README.md | 13 ++ plugins/shortcuts/dev/index.tsx | 26 ++++ plugins/shortcuts/package.json | 51 +++++++ plugins/shortcuts/src/AddShortcut.tsx | 121 +++++++++++++++ plugins/shortcuts/src/EditShortcut.tsx | 134 +++++++++++++++++ plugins/shortcuts/src/ShortcutForm.tsx | 140 ++++++++++++++++++ plugins/shortcuts/src/ShortcutIcon.tsx | 47 ++++++ plugins/shortcuts/src/ShortcutItem.tsx | 98 ++++++++++++ plugins/shortcuts/src/Shortcuts.tsx | 66 +++++++++ .../shortcuts/src/api/LocalStoredShortcuts.ts | 97 ++++++++++++ plugins/shortcuts/src/api/ShortcutApi.ts | 55 +++++++ plugins/shortcuts/src/api/index.ts | 19 +++ plugins/shortcuts/src/index.ts | 18 +++ plugins/shortcuts/src/plugin.test.ts | 22 +++ plugins/shortcuts/src/plugin.ts | 44 ++++++ plugins/shortcuts/src/setupTests.ts | 17 +++ plugins/shortcuts/src/types.ts | 26 ++++ yarn.lock | 8 +- 22 files changed, 1010 insertions(+), 1 deletion(-) create mode 100644 plugins/shortcuts/.eslintrc.js create mode 100644 plugins/shortcuts/README.md create mode 100644 plugins/shortcuts/dev/index.tsx create mode 100644 plugins/shortcuts/package.json create mode 100644 plugins/shortcuts/src/AddShortcut.tsx create mode 100644 plugins/shortcuts/src/EditShortcut.tsx create mode 100644 plugins/shortcuts/src/ShortcutForm.tsx create mode 100644 plugins/shortcuts/src/ShortcutIcon.tsx create mode 100644 plugins/shortcuts/src/ShortcutItem.tsx create mode 100644 plugins/shortcuts/src/Shortcuts.tsx create mode 100644 plugins/shortcuts/src/api/LocalStoredShortcuts.ts create mode 100644 plugins/shortcuts/src/api/ShortcutApi.ts create mode 100644 plugins/shortcuts/src/api/index.ts create mode 100644 plugins/shortcuts/src/index.ts create mode 100644 plugins/shortcuts/src/plugin.test.ts create mode 100644 plugins/shortcuts/src/plugin.ts create mode 100644 plugins/shortcuts/src/setupTests.ts create mode 100644 plugins/shortcuts/src/types.ts diff --git a/packages/app/package.json b/packages/app/package.json index 4d2dbe660a..f052d27109 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -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", diff --git a/packages/app/src/components/Root/Root.tsx b/packages/app/src/components/Root/Root.tsx index 454c0d2e84..9c62ec0b4d 100644 --- a/packages/app/src/components/Root/Root.tsx +++ b/packages/app/src/components/Root/Root.tsx @@ -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<{}>) => ( + + diff --git a/packages/app/src/plugins.ts b/packages/app/src/plugins.ts index 7dbea5b995..9ea887ac27 100644 --- a/packages/app/src/plugins.ts +++ b/packages/app/src/plugins.ts @@ -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'; diff --git a/plugins/shortcuts/.eslintrc.js b/plugins/shortcuts/.eslintrc.js new file mode 100644 index 0000000000..13573efa9c --- /dev/null +++ b/plugins/shortcuts/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], +}; diff --git a/plugins/shortcuts/README.md b/plugins/shortcuts/README.md new file mode 100644 index 0000000000..423f03948c --- /dev/null +++ b/plugins/shortcuts/README.md @@ -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. diff --git a/plugins/shortcuts/dev/index.tsx b/plugins/shortcuts/dev/index.tsx new file mode 100644 index 0000000000..0da3a90054 --- /dev/null +++ b/plugins/shortcuts/dev/index.tsx @@ -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: , + title: 'Root Page', + }) + .render(); diff --git a/plugins/shortcuts/package.json b/plugins/shortcuts/package.json new file mode 100644 index 0000000000..32fb41ab78 --- /dev/null +++ b/plugins/shortcuts/package.json @@ -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" + ] +} diff --git a/plugins/shortcuts/src/AddShortcut.tsx b/plugins/shortcuts/src/AddShortcut.tsx new file mode 100644 index 0000000000..ab845c63a8 --- /dev/null +++ b/plugins/shortcuts/src/AddShortcut.tsx @@ -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(); + const open = Boolean(anchorEl); + + const handleSave: SubmitHandler = async ({ url, title }) => { + const shortcut: Omit = { 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 ( + + + + Paste Current Url + + } + /> + + + + ); +}; diff --git a/plugins/shortcuts/src/EditShortcut.tsx b/plugins/shortcuts/src/EditShortcut.tsx new file mode 100644 index 0000000000..9220c9a73e --- /dev/null +++ b/plugins/shortcuts/src/EditShortcut.tsx @@ -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 = 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 ( + + + } + onClick={handleRemove} + > + Remove + + } + /> + + + + ); +}; diff --git a/plugins/shortcuts/src/ShortcutForm.tsx b/plugins/shortcuts/src/ShortcutForm.tsx new file mode 100644 index 0000000000..6d6e0679c3 --- /dev/null +++ b/plugins/shortcuts/src/ShortcutForm.tsx @@ -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; + onClose: () => void; +}; + +export const ShortcutForm = ({ formValues, onSave, onClose }: Props) => { + const classes = useStyles(); + + const { + handleSubmit, + reset, + control, + formState: { errors }, + } = useForm({ + mode: 'onChange', + defaultValues: { + url: formValues?.url ?? '', + title: formValues?.title ?? '', + }, + }); + + useEffect(() => { + reset(formValues); + }, [reset, formValues]); + + return ( + <> + + ( + + )} + /> + ( + + )} + /> + + + + + + + ); +}; diff --git a/plugins/shortcuts/src/ShortcutIcon.tsx b/plugins/shortcuts/src/ShortcutIcon.tsx new file mode 100644 index 0000000000..c737596647 --- /dev/null +++ b/plugins/shortcuts/src/ShortcutIcon.tsx @@ -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 ( + + + + {text} + + + ); +}; diff --git a/plugins/shortcuts/src/ShortcutItem.tsx b/plugins/shortcuts/src/ShortcutItem.tsx new file mode 100644 index 0000000000..a52194828c --- /dev/null +++ b/plugins/shortcuts/src/ShortcutItem.tsx @@ -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(); + + const handleClick = (event: React.MouseEvent) => { + 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 ( +
+ } + > + {displayEdit && ( + + + + )} + + +
+ ); +}; diff --git a/plugins/shortcuts/src/Shortcuts.tsx b/plugins/shortcuts/src/Shortcuts.tsx new file mode 100644 index 0000000000..4d65861bd9 --- /dev/null +++ b/plugins/shortcuts/src/Shortcuts.tsx @@ -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(); + const loading = Boolean(!shortcuts); + + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(undefined); + }; + + return ( + <> + + + {loading ? ( + + ) : ( + shortcuts?.map(shortcut => ( + + )) + )} + + ); +}; diff --git a/plugins/shortcuts/src/api/LocalStoredShortcuts.ts b/plugins/shortcuts/src/api/LocalStoredShortcuts.ts new file mode 100644 index 0000000000..2f84c0cf76 --- /dev/null +++ b/plugins/shortcuts/src/api/LocalStoredShortcuts.ts @@ -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) { + 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 + >(); + + private readonly observable = new ObservableImpl(subscriber => { + subscriber.next(this.get()); + this.subscribers.add(subscriber); + + return () => { + this.subscribers.delete(subscriber); + }; + }); + + private readonly THEME_MAP: Record = { + 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()); + } + } +} diff --git a/plugins/shortcuts/src/api/ShortcutApi.ts b/plugins/shortcuts/src/api/ShortcutApi.ts new file mode 100644 index 0000000000..b2b1f772e5 --- /dev/null +++ b/plugins/shortcuts/src/api/ShortcutApi.ts @@ -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({ + 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; + + /** + * Generates a unique id for the shortcut and then saves it. + */ + add(shortcut: Omit): Promise; + + /** + * Removes the shortcut. + */ + remove(shortcut: Shortcut): Promise; + + /** + * Finds an existing shortcut that matches the ID of the + * supplied shortcut and updates its values. + */ + update(shortcut: Shortcut): Promise; + + /** + * 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; +} diff --git a/plugins/shortcuts/src/api/index.ts b/plugins/shortcuts/src/api/index.ts new file mode 100644 index 0000000000..4705702cbe --- /dev/null +++ b/plugins/shortcuts/src/api/index.ts @@ -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'; diff --git a/plugins/shortcuts/src/index.ts b/plugins/shortcuts/src/index.ts new file mode 100644 index 0000000000..851178710f --- /dev/null +++ b/plugins/shortcuts/src/index.ts @@ -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'; diff --git a/plugins/shortcuts/src/plugin.test.ts b/plugins/shortcuts/src/plugin.test.ts new file mode 100644 index 0000000000..885b4e226e --- /dev/null +++ b/plugins/shortcuts/src/plugin.test.ts @@ -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(); + }); +}); diff --git a/plugins/shortcuts/src/plugin.ts b/plugins/shortcuts/src/plugin.ts new file mode 100644 index 0000000000..d33af76f33 --- /dev/null +++ b/plugins/shortcuts/src/plugin.ts @@ -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) }, + }), +); diff --git a/plugins/shortcuts/src/setupTests.ts b/plugins/shortcuts/src/setupTests.ts new file mode 100644 index 0000000000..0cec5b395d --- /dev/null +++ b/plugins/shortcuts/src/setupTests.ts @@ -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'; diff --git a/plugins/shortcuts/src/types.ts b/plugins/shortcuts/src/types.ts new file mode 100644 index 0000000000..33c52f4766 --- /dev/null +++ b/plugins/shortcuts/src/types.ts @@ -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; +}; diff --git a/yarn.lock b/yarn.lock index 000b4b55ed..f56a6931ea 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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== From c2c55e88488b4699c839e99ff6fce84bd1eb9b7e Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Tue, 20 Apr 2021 15:03:48 +0200 Subject: [PATCH 02/27] Add alertApi to mockApis Signed-off-by: Marcus Eide --- packages/test-utils/src/testUtils/mockApis.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/test-utils/src/testUtils/mockApis.ts b/packages/test-utils/src/testUtils/mockApis.ts index e05a8e6cac..0fe9a30926 100644 --- a/packages/test-utils/src/testUtils/mockApis.ts +++ b/packages/test-utils/src/testUtils/mockApis.ts @@ -18,10 +18,13 @@ import { storageApiRef, errorApiRef, createApiFactory, + AlertApiForwarder, + alertApiRef, } from '@backstage/core-api'; import { MockErrorApi, MockStorageApi } from './apis'; export const mockApis = [ createApiFactory(errorApiRef, new MockErrorApi()), createApiFactory(storageApiRef, MockStorageApi.create()), + createApiFactory(alertApiRef, new AlertApiForwarder()), ]; From 87d9b1170e1747ded9c65093ab97e96d9ab93f0d Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Tue, 20 Apr 2021 15:05:25 +0200 Subject: [PATCH 03/27] Add tests Signed-off-by: Marcus Eide --- plugins/shortcuts/src/AddShortcut.test.tsx | 113 +++++++++++++++ plugins/shortcuts/src/EditShortcut.test.tsx | 130 ++++++++++++++++++ plugins/shortcuts/src/ShortcutForm.test.tsx | 73 ++++++++++ plugins/shortcuts/src/ShortcutItem.test.tsx | 128 +++++++++++++++++ plugins/shortcuts/src/Shortcuts.test.tsx | 42 ++++++ .../src/api/LocalStoredShortcuts.test.ts | 82 +++++++++++ 6 files changed, 568 insertions(+) create mode 100644 plugins/shortcuts/src/AddShortcut.test.tsx create mode 100644 plugins/shortcuts/src/EditShortcut.test.tsx create mode 100644 plugins/shortcuts/src/ShortcutForm.test.tsx create mode 100644 plugins/shortcuts/src/ShortcutItem.test.tsx create mode 100644 plugins/shortcuts/src/Shortcuts.test.tsx create mode 100644 plugins/shortcuts/src/api/LocalStoredShortcuts.test.ts diff --git a/plugins/shortcuts/src/AddShortcut.test.tsx b/plugins/shortcuts/src/AddShortcut.test.tsx new file mode 100644 index 0000000000..690bb2c59e --- /dev/null +++ b/plugins/shortcuts/src/AddShortcut.test.tsx @@ -0,0 +1,113 @@ +/* + * 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 { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { AddShortcut } from './AddShortcut'; +import { LocalStoredShortcuts } from './api'; +import { MockStorageApi, wrapInTestApp } from '@backstage/test-utils'; +import { AlertDisplay } from '@backstage/core'; + +describe('AddShortcut', () => { + const api = new LocalStoredShortcuts(MockStorageApi.create()); + + const props = { + onClose: jest.fn(), + anchorEl: document.createElement('div'), + api, + }; + + beforeEach(() => { + jest.clearAllMocks(); + document.title = 'some document title'; + }); + + it('displays the title', async () => { + render(wrapInTestApp()); + + await waitFor(() => { + expect(screen.getByText('Add Shortcut')).toBeInTheDocument(); + }); + }); + + it('closes the popup', async () => { + render(wrapInTestApp()); + + fireEvent.click(screen.getByText('Cancel')); + await waitFor(() => { + expect(props.onClose).toHaveBeenCalledTimes(1); + }); + }); + + it('saves the input', async () => { + const spy = jest.spyOn(api, 'add'); + + render(wrapInTestApp()); + + const urlInput = screen.getByPlaceholderText('Enter a URL'); + const titleInput = screen.getByPlaceholderText('Enter a display name'); + fireEvent.change(urlInput, { target: { value: '/some-url' } }); + fireEvent.change(titleInput, { target: { value: 'some title' } }); + + fireEvent.click(screen.getByText('Save')); + await waitFor(() => { + expect(spy).toBeCalledWith({ + title: 'some title', + url: '/some-url', + }); + }); + }); + + it('pastes the values', async () => { + const spy = jest.spyOn(api, 'add'); + + render( + wrapInTestApp(, { + routeEntries: ['/some-initial-url'], + }), + ); + + fireEvent.click(screen.getByText('Paste Current Url')); + fireEvent.click(screen.getByText('Save')); + await waitFor(() => { + expect(spy).toBeCalledWith({ + title: 'some document title', + url: '/some-initial-url', + }); + }); + }); + + it('displays errors', async () => { + jest.spyOn(api, 'add').mockRejectedValueOnce(new Error('some add error')); + + render( + wrapInTestApp( + <> + + + , + ), + ); + + fireEvent.click(screen.getByText('Paste Current Url')); + fireEvent.click(screen.getByText('Save')); + await waitFor(() => { + expect( + screen.getByText('Could not add shortcut: some add error'), + ).toBeInTheDocument(); + }); + }); +}); diff --git a/plugins/shortcuts/src/EditShortcut.test.tsx b/plugins/shortcuts/src/EditShortcut.test.tsx new file mode 100644 index 0000000000..bf2396e194 --- /dev/null +++ b/plugins/shortcuts/src/EditShortcut.test.tsx @@ -0,0 +1,130 @@ +/* + * 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 { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { EditShortcut } from './EditShortcut'; +import { Shortcut } from './types'; +import { LocalStoredShortcuts } from './api'; +import { MockStorageApi, wrapInTestApp } from '@backstage/test-utils'; +import { AlertDisplay } from '@backstage/core'; + +describe('EditShortcut', () => { + const shortcut: Shortcut = { + id: 'id', + url: '/some-url', + title: 'some title', + }; + const api = new LocalStoredShortcuts(MockStorageApi.create()); + + const props = { + onClose: jest.fn(), + anchorEl: document.createElement('div'), + shortcut, + api, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('displays the title', async () => { + render(wrapInTestApp()); + + await waitFor(() => { + expect(screen.getByText('Edit Shortcut')).toBeInTheDocument(); + }); + }); + + it('closes the popup', async () => { + render(wrapInTestApp()); + + fireEvent.click(screen.getByText('Cancel')); + await waitFor(() => { + expect(props.onClose).toHaveBeenCalledTimes(1); + }); + }); + + it('updates the shortcut', async () => { + const spy = jest.spyOn(api, 'update'); + + render(wrapInTestApp()); + + const urlInput = screen.getByPlaceholderText('Enter a URL'); + const titleInput = screen.getByPlaceholderText('Enter a display name'); + fireEvent.change(urlInput, { target: { value: '/some-new-url' } }); + fireEvent.change(titleInput, { target: { value: 'some new title' } }); + + fireEvent.click(screen.getByText('Save')); + await waitFor(() => { + expect(spy).toBeCalledWith({ + id: 'id', + title: 'some new title', + url: '/some-new-url', + }); + expect(props.onClose).toHaveBeenCalledTimes(1); + }); + }); + + it('removes the shortcut', async () => { + const spy = jest.spyOn(api, 'remove'); + + render(wrapInTestApp()); + + fireEvent.click(screen.getByText('Remove')); + await waitFor(() => { + expect(spy).toBeCalledWith({ + id: 'id', + title: 'some title', + url: '/some-url', + }); + }); + }); + + it('displays errors', async () => { + jest + .spyOn(api, 'update') + .mockRejectedValueOnce(new Error('some update error')); + + jest + .spyOn(api, 'remove') + .mockRejectedValueOnce(new Error('some remove error')); + + render( + wrapInTestApp( + <> + + + , + ), + ); + + fireEvent.click(screen.getByText('Save')); + await waitFor(() => { + expect( + screen.getByText('Could not update shortcut: some update error'), + ).toBeInTheDocument(); + }); + fireEvent.click(screen.getByTestId('error-button-close')); + + fireEvent.click(screen.getByText('Remove')); + await waitFor(() => { + expect( + screen.getByText('Could not delete shortcut: some remove error'), + ).toBeInTheDocument(); + }); + }); +}); diff --git a/plugins/shortcuts/src/ShortcutForm.test.tsx b/plugins/shortcuts/src/ShortcutForm.test.tsx new file mode 100644 index 0000000000..4586c484d0 --- /dev/null +++ b/plugins/shortcuts/src/ShortcutForm.test.tsx @@ -0,0 +1,73 @@ +/* + * 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 { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { ShortcutForm } from './ShortcutForm'; +import { wrapInTestApp } from '@backstage/test-utils'; + +describe('ShortcutForm', () => { + const props = { + onSave: jest.fn(), + onClose: jest.fn(), + }; + + it('displays validation messages', async () => { + render(wrapInTestApp()); + + const urlInput = screen.getByPlaceholderText('Enter a URL'); + const titleInput = screen.getByPlaceholderText('Enter a display name'); + fireEvent.change(urlInput, { target: { value: 'url' } }); + fireEvent.change(titleInput, { target: { value: 't' } }); + + fireEvent.click(screen.getByText('Save')); + await waitFor(() => { + expect( + screen.getByText('Must be a relative URL (starts with a /)'), + ).toBeInTheDocument(); + expect( + screen.getByText('Must be at least 2 characters'), + ).toBeInTheDocument(); + }); + }); + + it('calls the save handler', async () => { + render( + wrapInTestApp( + , + ), + ); + + fireEvent.click(screen.getByText('Save')); + await waitFor(() => { + expect(props.onSave).toHaveBeenCalledWith( + expect.objectContaining({ title: 'some title', url: '/some-url' }), + expect.anything(), + ); + }); + }); + + it('calls the close handler', async () => { + render(wrapInTestApp()); + + fireEvent.click(screen.getByText('Cancel')); + await waitFor(() => { + expect(props.onClose).toHaveBeenCalled(); + }); + }); +}); diff --git a/plugins/shortcuts/src/ShortcutItem.test.tsx b/plugins/shortcuts/src/ShortcutItem.test.tsx new file mode 100644 index 0000000000..219771909d --- /dev/null +++ b/plugins/shortcuts/src/ShortcutItem.test.tsx @@ -0,0 +1,128 @@ +/* + * 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 { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { ShortcutItem } from './ShortcutItem'; +import { Shortcut } from './types'; +import { SidebarContext } from '@backstage/core'; +import { LocalStoredShortcuts } from './api'; +import { MockStorageApi, wrapInTestApp } from '@backstage/test-utils'; +import { pageTheme } from '@backstage/theme'; + +describe('ShortcutItem', () => { + const shortcut: Shortcut = { + id: 'id', + url: '/some-url', + title: 'some title', + }; + const api = new LocalStoredShortcuts(MockStorageApi.create()); + + it('displays the shortcut', async () => { + render( + wrapInTestApp( + + + , + ), + ); + await waitFor(() => { + expect(screen.getByText('ST')).toBeInTheDocument(); + expect(screen.getByText('some title')).toBeInTheDocument(); + }); + }); + + it('calculates the shortcut text correctly', async () => { + const shortcut1: Shortcut = { + id: 'id1', + url: '/some-url', + title: 'onetitle', + }; + const shortcut2: Shortcut = { + id: 'id2', + url: '/some-url', + title: 'two title', + }; + const shortcut3: Shortcut = { + id: 'id3', + url: '/some-url', + title: 'more | title words', + }; + + const { rerender } = render( + wrapInTestApp(), + ); + + await waitFor(() => { + expect(screen.getByText('On')).toBeInTheDocument(); + }); + + rerender(wrapInTestApp()); + await waitFor(() => { + expect(screen.getByText('TT')).toBeInTheDocument(); + }); + + rerender(wrapInTestApp()); + await waitFor(() => { + expect(screen.getByText('MT')).toBeInTheDocument(); + }); + }); + + it('displays the edit icon on hover', async () => { + render( + wrapInTestApp( + + + , + ), + ); + + fireEvent.mouseOver(screen.getByText('ST')); + await waitFor(() => { + expect(screen.getByTestId('edit')).toBeInTheDocument(); + }); + + fireEvent.mouseOut(screen.getByText('ST')); + await waitFor(() => { + expect(screen.queryByTestId('edit')).not.toBeInTheDocument(); + }); + }); + + it('gets the color based on the theme', async () => { + const { rerender } = render( + wrapInTestApp(), + ); + + await waitFor(() => { + expect(document.querySelector('circle')?.getAttribute('fill')).toEqual( + pageTheme.tool.colors[0], + ); + }); + + const newShortcut: Shortcut = { + id: 'id', + url: '/catalog', + title: 'some title', + }; + rerender(wrapInTestApp()); + + await waitFor(() => { + expect(document.querySelector('circle')?.getAttribute('fill')).toEqual( + pageTheme.home.colors[0], + ); + }); + }); +}); diff --git a/plugins/shortcuts/src/Shortcuts.test.tsx b/plugins/shortcuts/src/Shortcuts.test.tsx new file mode 100644 index 0000000000..088a3705d9 --- /dev/null +++ b/plugins/shortcuts/src/Shortcuts.test.tsx @@ -0,0 +1,42 @@ +/* + * 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 { SidebarContext, ApiProvider, ApiRegistry } from '@backstage/core'; +import { wrapInTestApp, MockStorageApi } from '@backstage/test-utils'; +import { render, screen, waitFor } from '@testing-library/react'; +import { Shortcuts } from './Shortcuts'; +import { LocalStoredShortcuts, shortcutsApiRef } from './api'; + +const apis = ApiRegistry.from([ + [shortcutsApiRef, new LocalStoredShortcuts(MockStorageApi.create())], +]); + +describe('Shortcuts', () => { + it('displays an add button', async () => { + render( + wrapInTestApp( + + + + + , + ), + ); + await waitFor(() => !screen.queryByTestId('progress')); + expect(screen.getByText('Add Shortcuts')).toBeInTheDocument(); + }); +}); diff --git a/plugins/shortcuts/src/api/LocalStoredShortcuts.test.ts b/plugins/shortcuts/src/api/LocalStoredShortcuts.test.ts new file mode 100644 index 0000000000..6fd45d73e5 --- /dev/null +++ b/plugins/shortcuts/src/api/LocalStoredShortcuts.test.ts @@ -0,0 +1,82 @@ +/* + * 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 { MockStorageApi } from '@backstage/test-utils'; +import { pageTheme } from '@backstage/theme'; +import { Shortcut } from '../types'; +import { LocalStoredShortcuts } from './LocalStoredShortcuts'; +import { ShortcutApi } from './ShortcutApi'; + +describe('LocalStoredShortcuts', () => { + // eslint-disable-next-line jest/no-done-callback + it('should observe shortcuts', async done => { + const shortcutApi: ShortcutApi = new LocalStoredShortcuts( + MockStorageApi.create(), + ); + const shortcut: Shortcut = { id: 'id', title: 'title', url: '/url' }; + + await shortcutApi.add(shortcut); + shortcutApi.observe().subscribe(data => { + expect(data).toEqual( + expect.arrayContaining([{ ...shortcut, id: expect.anything() }]), + ); + done(); + }); + }); + + it('should add shortcuts with ids', async () => { + const storageApi = MockStorageApi.create(); + const shortcutApi: ShortcutApi = new LocalStoredShortcuts(storageApi); + const shortcut: Omit = { title: 'title', url: '/url' }; + const spy = jest.spyOn(storageApi, 'set'); + + await shortcutApi.add(shortcut); + expect(spy).toHaveBeenCalledWith( + 'items', + expect.objectContaining([{ ...shortcut, id: expect.anything() }]), + ); + }); + + it('should update shortcuts', async () => { + const storageApi = MockStorageApi.create(); + const shortcutApi: ShortcutApi = new LocalStoredShortcuts(storageApi); + const shortcut: Shortcut = { id: 'someid', title: 'title', url: '/url' }; + const spy = jest.spyOn(storageApi, 'set'); + + await shortcutApi.update(shortcut); + expect(spy).toHaveBeenCalledWith( + 'items', + expect.objectContaining([shortcut]), + ); + }); + + it('should remove shortcuts', async () => { + const storageApi = MockStorageApi.create(); + const shortcutApi: ShortcutApi = new LocalStoredShortcuts(storageApi); + const shortcut: Shortcut = { id: 'someid', title: 'title', url: '/url' }; + const spy = jest.spyOn(storageApi, 'set'); + + await shortcutApi.remove(shortcut); + expect(spy).toHaveBeenCalledWith('items', []); + }); + + it('should get a color', () => { + const storageApi = MockStorageApi.create(); + const shortcutApi: ShortcutApi = new LocalStoredShortcuts(storageApi); + + expect(shortcutApi.getColor('/catalog')).toEqual(pageTheme.home.colors[0]); + }); +}); From f9a100f39c924aeb92664e15356e6dfa600ae407 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Tue, 20 Apr 2021 16:32:51 +0200 Subject: [PATCH 04/27] Remove unintentional export in plugins.ts Signed-off-by: Marcus Eide --- packages/app/src/plugins.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/app/src/plugins.ts b/packages/app/src/plugins.ts index 9ea887ac27..e02e30e5d9 100644 --- a/packages/app/src/plugins.ts +++ b/packages/app/src/plugins.ts @@ -17,5 +17,4 @@ // 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'; From 421563440800c08204af333cea107fe6a70aa20f Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Wed, 21 Apr 2021 09:17:34 +0200 Subject: [PATCH 05/27] Dependencies Signed-off-by: Marcus Eide --- plugins/shortcuts/package.json | 2 +- yarn.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/shortcuts/package.json b/plugins/shortcuts/package.json index 32fb41ab78..99e824e835 100644 --- a/plugins/shortcuts/package.json +++ b/plugins/shortcuts/package.json @@ -24,7 +24,6 @@ "@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", @@ -42,6 +41,7 @@ "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", + "@types/zen-observable": "^0.8.2", "cross-fetch": "^3.0.6", "msw": "^0.21.2" }, diff --git a/yarn.lock b/yarn.lock index f56a6931ea..3c563eee9e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6937,7 +6937,7 @@ resolved "https://registry.npmjs.org/@types/yup/-/yup-0.29.8.tgz#83db15735987db9fe5a38772a0fb9500e3c5bf39" integrity sha512-MBSp62AjB1KrSOI3gX9GekddXU5YYQAVA93+aSl78biBqoSzxg876aQY2KJK5Gnfbpqq7O2cadVX5kPAtBqIXw== -"@types/zen-observable@^0.8.0": +"@types/zen-observable@^0.8.0", "@types/zen-observable@^0.8.2": version "0.8.2" resolved "https://registry.npmjs.org/@types/zen-observable/-/zen-observable-0.8.2.tgz#808c9fa7e4517274ed555fa158f2de4b4f468e71" integrity sha512-HrCIVMLjE1MOozVoD86622S7aunluLb2PJdPfb3nYiEtohm8mIB/vyv0Fd37AdeMFrTUQXEunw78YloMA3Qilg== From e05f09698c2210d763d98860d5267c8b51d4e648 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Wed, 21 Apr 2021 10:00:15 +0200 Subject: [PATCH 06/27] Update alert messages Signed-off-by: Marcus Eide --- plugins/shortcuts/src/AddShortcut.test.tsx | 4 ++-- plugins/shortcuts/src/AddShortcut.tsx | 4 ++-- plugins/shortcuts/src/EditShortcut.tsx | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/shortcuts/src/AddShortcut.test.tsx b/plugins/shortcuts/src/AddShortcut.test.tsx index 690bb2c59e..cee5d8cf28 100644 --- a/plugins/shortcuts/src/AddShortcut.test.tsx +++ b/plugins/shortcuts/src/AddShortcut.test.tsx @@ -80,7 +80,7 @@ describe('AddShortcut', () => { }), ); - fireEvent.click(screen.getByText('Paste Current Url')); + fireEvent.click(screen.getByText('Use current page')); fireEvent.click(screen.getByText('Save')); await waitFor(() => { expect(spy).toBeCalledWith({ @@ -102,7 +102,7 @@ describe('AddShortcut', () => { ), ); - fireEvent.click(screen.getByText('Paste Current Url')); + fireEvent.click(screen.getByText('Use current page')); fireEvent.click(screen.getByText('Save')); await waitFor(() => { expect( diff --git a/plugins/shortcuts/src/AddShortcut.tsx b/plugins/shortcuts/src/AddShortcut.tsx index ab845c63a8..74a0653b4c 100644 --- a/plugins/shortcuts/src/AddShortcut.tsx +++ b/plugins/shortcuts/src/AddShortcut.tsx @@ -60,7 +60,7 @@ export const AddShortcut = ({ onClose, anchorEl, api }: Props) => { try { await api.add(shortcut); alertApi.post({ - message: 'Successfully added shortcut', + message: `Added shortcut '${title}' to your sidebar`, severity: 'success', }); } catch (error) { @@ -106,7 +106,7 @@ export const AddShortcut = ({ onClose, anchorEl, api }: Props) => { color="primary" onClick={handlePaste} > - Paste Current Url + Use current page } /> diff --git a/plugins/shortcuts/src/EditShortcut.tsx b/plugins/shortcuts/src/EditShortcut.tsx index 9220c9a73e..8b98406703 100644 --- a/plugins/shortcuts/src/EditShortcut.tsx +++ b/plugins/shortcuts/src/EditShortcut.tsx @@ -63,7 +63,7 @@ export const EditShortcut = ({ shortcut, onClose, anchorEl, api }: Props) => { try { await api.update(newShortcut); alertApi.post({ - message: 'Successfully updated shortcut', + message: `Updated shortcut '${title}'`, severity: 'success', }); } catch (error) { @@ -80,7 +80,7 @@ export const EditShortcut = ({ shortcut, onClose, anchorEl, api }: Props) => { try { await api.remove(shortcut); alertApi.post({ - message: 'Successfully deleted shortcut', + message: `Removed shortcut '${shortcut.title}' from your sidebar`, severity: 'success', }); } catch (error) { From 325d8da1645a7579b4ae3dad74e4bad651e5c3f0 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Wed, 21 Apr 2021 10:21:33 +0200 Subject: [PATCH 07/27] Rename observe() to shortcut Signed-off-by: Marcus Eide --- plugins/shortcuts/src/Shortcuts.tsx | 2 +- plugins/shortcuts/src/api/LocalStoredShortcuts.test.ts | 2 +- plugins/shortcuts/src/api/LocalStoredShortcuts.ts | 2 +- plugins/shortcuts/src/api/ShortcutApi.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/shortcuts/src/Shortcuts.tsx b/plugins/shortcuts/src/Shortcuts.tsx index 4d65861bd9..0e8804cd39 100644 --- a/plugins/shortcuts/src/Shortcuts.tsx +++ b/plugins/shortcuts/src/Shortcuts.tsx @@ -25,7 +25,7 @@ import { shortcutsApiRef } from './api'; export const Shortcuts = () => { const shortcutApi = useApi(shortcutsApiRef); const shortcuts = useObservable( - useMemo(() => shortcutApi.observe(), [shortcutApi]), + useMemo(() => shortcutApi.shortcut$(), [shortcutApi]), ); const [anchorEl, setAnchorEl] = React.useState(); const loading = Boolean(!shortcuts); diff --git a/plugins/shortcuts/src/api/LocalStoredShortcuts.test.ts b/plugins/shortcuts/src/api/LocalStoredShortcuts.test.ts index 6fd45d73e5..94f63f846a 100644 --- a/plugins/shortcuts/src/api/LocalStoredShortcuts.test.ts +++ b/plugins/shortcuts/src/api/LocalStoredShortcuts.test.ts @@ -29,7 +29,7 @@ describe('LocalStoredShortcuts', () => { const shortcut: Shortcut = { id: 'id', title: 'title', url: '/url' }; await shortcutApi.add(shortcut); - shortcutApi.observe().subscribe(data => { + shortcutApi.shortcut$().subscribe(data => { expect(data).toEqual( expect.arrayContaining([{ ...shortcut, id: expect.anything() }]), ); diff --git a/plugins/shortcuts/src/api/LocalStoredShortcuts.ts b/plugins/shortcuts/src/api/LocalStoredShortcuts.ts index 2f84c0cf76..c606fb5f9a 100644 --- a/plugins/shortcuts/src/api/LocalStoredShortcuts.ts +++ b/plugins/shortcuts/src/api/LocalStoredShortcuts.ts @@ -27,7 +27,7 @@ import { Shortcut } from '../types'; export class LocalStoredShortcuts implements ShortcutApi { constructor(private readonly storageApi: StorageApi) {} - observe() { + shortcut$() { return this.observable; } diff --git a/plugins/shortcuts/src/api/ShortcutApi.ts b/plugins/shortcuts/src/api/ShortcutApi.ts index b2b1f772e5..bd6eb0f196 100644 --- a/plugins/shortcuts/src/api/ShortcutApi.ts +++ b/plugins/shortcuts/src/api/ShortcutApi.ts @@ -27,7 +27,7 @@ export interface ShortcutApi { /** * Returns an Observable that will subscribe to changes. */ - observe(): Observable; + shortcut$(): Observable; /** * Generates a unique id for the shortcut and then saves it. From 55dce861535d58dba8670f64ec8559f0346a7a1c Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Wed, 21 Apr 2021 13:54:41 +0200 Subject: [PATCH 08/27] Keep wordboundary when getting the icon text Signed-off-by: Marcus Eide --- plugins/shortcuts/src/ShortcutItem.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/shortcuts/src/ShortcutItem.tsx b/plugins/shortcuts/src/ShortcutItem.tsx index a52194828c..3aec4bb3b4 100644 --- a/plugins/shortcuts/src/ShortcutItem.tsx +++ b/plugins/shortcuts/src/ShortcutItem.tsx @@ -36,7 +36,7 @@ const getIconText = (title: string) => 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, ' ') + .replace(/\B\W/g, '') .split(' ') .map(s => s[0]) .join('') From 303d548d745ce57076585905e67aabae419145e9 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Wed, 21 Apr 2021 13:57:49 +0200 Subject: [PATCH 09/27] Use all available vertical space in the sidebar and overflow with scroll if needed Signed-off-by: Marcus Eide --- plugins/shortcuts/src/Shortcuts.tsx | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/plugins/shortcuts/src/Shortcuts.tsx b/plugins/shortcuts/src/Shortcuts.tsx index 0e8804cd39..2c49245e76 100644 --- a/plugins/shortcuts/src/Shortcuts.tsx +++ b/plugins/shortcuts/src/Shortcuts.tsx @@ -17,12 +17,21 @@ import React, { useMemo } from 'react'; import { useObservable } from 'react-use'; import { Progress, SidebarItem, useApi } from '@backstage/core'; +import { makeStyles } from '@material-ui/core'; import PlayListAddIcon from '@material-ui/icons/PlaylistAdd'; import { ShortcutItem } from './ShortcutItem'; import { AddShortcut } from './AddShortcut'; import { shortcutsApiRef } from './api'; +const useStyles = makeStyles({ + root: { + flex: '1 1 auto', + overflow: 'scroll', + }, +}); + export const Shortcuts = () => { + const classes = useStyles(); const shortcutApi = useApi(shortcutsApiRef); const shortcuts = useObservable( useMemo(() => shortcutApi.shortcut$(), [shortcutApi]), @@ -39,7 +48,7 @@ export const Shortcuts = () => { }; return ( - <> +
{ /> )) )} - +
); }; From aac78e0a7bbc0b0a8a83be7c322bf2bc77790d63 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Wed, 21 Apr 2021 14:37:40 +0200 Subject: [PATCH 10/27] Sort on title Signed-off-by: Marcus Eide --- plugins/shortcuts/src/api/LocalStoredShortcuts.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/shortcuts/src/api/LocalStoredShortcuts.ts b/plugins/shortcuts/src/api/LocalStoredShortcuts.ts index c606fb5f9a..377f8e9190 100644 --- a/plugins/shortcuts/src/api/LocalStoredShortcuts.ts +++ b/plugins/shortcuts/src/api/LocalStoredShortcuts.ts @@ -84,7 +84,7 @@ export class LocalStoredShortcuts implements ShortcutApi { private get() { return ( (this.storageApi.get('items') as Shortcut[])?.sort((a, b) => - a.id >= b.id ? 1 : -1, + a.title >= b.title ? 1 : -1, ) ?? [] ); } From a097131d0d739355dee5883a5ead2ab7daea57d5 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Wed, 21 Apr 2021 15:28:08 +0200 Subject: [PATCH 11/27] Use Observable from @backstage/core Signed-off-by: Marcus Eide --- plugins/shortcuts/package.json | 1 - plugins/shortcuts/src/api/ShortcutApi.ts | 3 +-- yarn.lock | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/plugins/shortcuts/package.json b/plugins/shortcuts/package.json index 99e824e835..5795dcd922 100644 --- a/plugins/shortcuts/package.json +++ b/plugins/shortcuts/package.json @@ -41,7 +41,6 @@ "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", - "@types/zen-observable": "^0.8.2", "cross-fetch": "^3.0.6", "msw": "^0.21.2" }, diff --git a/plugins/shortcuts/src/api/ShortcutApi.ts b/plugins/shortcuts/src/api/ShortcutApi.ts index bd6eb0f196..826988d01f 100644 --- a/plugins/shortcuts/src/api/ShortcutApi.ts +++ b/plugins/shortcuts/src/api/ShortcutApi.ts @@ -14,8 +14,7 @@ * limitations under the License. */ -import Observable from 'zen-observable'; -import { createApiRef } from '@backstage/core'; +import { createApiRef, Observable } from '@backstage/core'; import { Shortcut } from '../types'; export const shortcutsApiRef = createApiRef({ diff --git a/yarn.lock b/yarn.lock index 3c563eee9e..f56a6931ea 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6937,7 +6937,7 @@ resolved "https://registry.npmjs.org/@types/yup/-/yup-0.29.8.tgz#83db15735987db9fe5a38772a0fb9500e3c5bf39" integrity sha512-MBSp62AjB1KrSOI3gX9GekddXU5YYQAVA93+aSl78biBqoSzxg876aQY2KJK5Gnfbpqq7O2cadVX5kPAtBqIXw== -"@types/zen-observable@^0.8.0", "@types/zen-observable@^0.8.2": +"@types/zen-observable@^0.8.0": version "0.8.2" resolved "https://registry.npmjs.org/@types/zen-observable/-/zen-observable-0.8.2.tgz#808c9fa7e4517274ed555fa158f2de4b4f468e71" integrity sha512-HrCIVMLjE1MOozVoD86622S7aunluLb2PJdPfb3nYiEtohm8mIB/vyv0Fd37AdeMFrTUQXEunw78YloMA3Qilg== From 947e45cefd04b06d7dead488f651925a19014a8e Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Wed, 21 Apr 2021 16:04:51 +0200 Subject: [PATCH 12/27] Add back @types/zen-observable Signed-off-by: Marcus Eide --- plugins/shortcuts/package.json | 1 + yarn.lock | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/shortcuts/package.json b/plugins/shortcuts/package.json index 5795dcd922..bcac92fcb0 100644 --- a/plugins/shortcuts/package.json +++ b/plugins/shortcuts/package.json @@ -24,6 +24,7 @@ "@backstage/theme": "^0.2.5", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", + "@types/zen-observable": "^0.8.2", "react": "^16.13.1", "react-dom": "^16.13.1", "react-hook-form": "^7.1.1", diff --git a/yarn.lock b/yarn.lock index f56a6931ea..3c563eee9e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6937,7 +6937,7 @@ resolved "https://registry.npmjs.org/@types/yup/-/yup-0.29.8.tgz#83db15735987db9fe5a38772a0fb9500e3c5bf39" integrity sha512-MBSp62AjB1KrSOI3gX9GekddXU5YYQAVA93+aSl78biBqoSzxg876aQY2KJK5Gnfbpqq7O2cadVX5kPAtBqIXw== -"@types/zen-observable@^0.8.0": +"@types/zen-observable@^0.8.0", "@types/zen-observable@^0.8.2": version "0.8.2" resolved "https://registry.npmjs.org/@types/zen-observable/-/zen-observable-0.8.2.tgz#808c9fa7e4517274ed555fa158f2de4b4f468e71" integrity sha512-HrCIVMLjE1MOozVoD86622S7aunluLb2PJdPfb3nYiEtohm8mIB/vyv0Fd37AdeMFrTUQXEunw78YloMA3Qilg== From 15067b0566f6f81cf9253e978a104909a05d4ca0 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Thu, 22 Apr 2021 09:53:55 +0200 Subject: [PATCH 13/27] Add back @material-ui/lab to pass diff check Signed-off-by: Marcus Eide --- plugins/shortcuts/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/shortcuts/package.json b/plugins/shortcuts/package.json index bcac92fcb0..b3d8d7f4c1 100644 --- a/plugins/shortcuts/package.json +++ b/plugins/shortcuts/package.json @@ -24,6 +24,7 @@ "@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", "@types/zen-observable": "^0.8.2", "react": "^16.13.1", "react-dom": "^16.13.1", From f8eb7cacc7c130a3e3a1df5e5efed29ca018eea5 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Thu, 22 Apr 2021 10:33:40 +0200 Subject: [PATCH 14/27] Provide implementation of the api in the app instead Signed-off-by: Marcus Eide --- packages/app/src/apis.ts | 14 ++++++++++++++ plugins/shortcuts/src/plugin.ts | 19 +------------------ 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/packages/app/src/apis.ts b/packages/app/src/apis.ts index ccc576e727..6e643193db 100644 --- a/packages/app/src/apis.ts +++ b/packages/app/src/apis.ts @@ -20,6 +20,7 @@ import { createApiFactory, errorApiRef, githubAuthApiRef, + WebStorage, } from '@backstage/core'; import { ScmIntegrationsApi, @@ -33,6 +34,10 @@ import { graphQlBrowseApiRef, GraphQLEndpoints, } from '@backstage/plugin-graphiql'; +import { + LocalStoredShortcuts, + shortcutsApiRef, +} from '@backstage/plugin-shortcuts'; export const apis: AnyApiFactory[] = [ createApiFactory({ @@ -61,4 +66,13 @@ export const apis: AnyApiFactory[] = [ }), createApiFactory(costInsightsApiRef, new ExampleCostInsightsClient()), + + createApiFactory({ + api: shortcutsApiRef, + deps: { errorApi: errorApiRef }, + factory: ({ errorApi }) => + new LocalStoredShortcuts( + WebStorage.create({ namespace: '@backstage/shortcuts', errorApi }), + ), + }), ]; diff --git a/plugins/shortcuts/src/plugin.ts b/plugins/shortcuts/src/plugin.ts index d33af76f33..b81062505a 100644 --- a/plugins/shortcuts/src/plugin.ts +++ b/plugins/shortcuts/src/plugin.ts @@ -14,27 +14,10 @@ * limitations under the License. */ -import { - createApiFactory, - createComponentExtension, - createPlugin, - errorApiRef, - WebStorage, -} from '@backstage/core'; -import { shortcutsApiRef, LocalStoredShortcuts } from './api'; +import { createComponentExtension, createPlugin } from '@backstage/core'; 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( From e15241ea1d8f392bee2c9ff7161e21fdb55d28f1 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Thu, 22 Apr 2021 10:40:10 +0200 Subject: [PATCH 15/27] Update README Signed-off-by: Marcus Eide --- plugins/shortcuts/README.md | 40 ++++++++++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/plugins/shortcuts/README.md b/plugins/shortcuts/README.md index 423f03948c..2848ece0e8 100644 --- a/plugins/shortcuts/README.md +++ b/plugins/shortcuts/README.md @@ -1,13 +1,39 @@ # shortcuts -Welcome to the shortcuts plugin! +`shortcuts-plugin` allows a user to have easy access to pages within a Backstage app by storing them as "shortcuts" in the Sidebar. -_This plugin was created through the Backstage CLI_ +## Usage -## Getting started +Add the `` component within your ``: -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). +```ts +import { Shortcuts } from '@backstage/plugin-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. + + {/* ... */} + + + +; +``` + +The plugin exports a `shortcutApiRef` and an implementation of the `ShortcutApi` that uses `localStorage` for storage, that you can use to get started quickly. To use it add it to your app's `apis.ts`: + +```ts +import { + LocalStoredShortcuts, + shortcutsApiRef, +} from '@backstage/plugin-shortcuts'; + +export const apis = [ + // ... + createApiFactory({ + api: shortcutsApiRef, + deps: { errorApi: errorApiRef }, + factory: ({ errorApi }) => + new LocalStoredShortcuts( + WebStorage.create({ namespace: '@backstage/shortcuts', errorApi }), + ), + }), +]; +``` From a4c5361c0ed8daf7e1b81fb56a842e9c585c003e Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Fri, 30 Apr 2021 09:42:08 +0200 Subject: [PATCH 16/27] Use storageApi.forBucket and add back a default implementation in the plugin Signed-off-by: Marcus Eide --- packages/app/src/apis.ts | 14 -------------- plugins/shortcuts/README.md | 17 +++++++---------- plugins/shortcuts/src/plugin.ts | 16 +++++++++++++++- 3 files changed, 22 insertions(+), 25 deletions(-) diff --git a/packages/app/src/apis.ts b/packages/app/src/apis.ts index 6e643193db..ccc576e727 100644 --- a/packages/app/src/apis.ts +++ b/packages/app/src/apis.ts @@ -20,7 +20,6 @@ import { createApiFactory, errorApiRef, githubAuthApiRef, - WebStorage, } from '@backstage/core'; import { ScmIntegrationsApi, @@ -34,10 +33,6 @@ import { graphQlBrowseApiRef, GraphQLEndpoints, } from '@backstage/plugin-graphiql'; -import { - LocalStoredShortcuts, - shortcutsApiRef, -} from '@backstage/plugin-shortcuts'; export const apis: AnyApiFactory[] = [ createApiFactory({ @@ -66,13 +61,4 @@ export const apis: AnyApiFactory[] = [ }), createApiFactory(costInsightsApiRef, new ExampleCostInsightsClient()), - - createApiFactory({ - api: shortcutsApiRef, - deps: { errorApi: errorApiRef }, - factory: ({ errorApi }) => - new LocalStoredShortcuts( - WebStorage.create({ namespace: '@backstage/shortcuts', errorApi }), - ), - }), ]; diff --git a/plugins/shortcuts/README.md b/plugins/shortcuts/README.md index 2848ece0e8..dfe8577190 100644 --- a/plugins/shortcuts/README.md +++ b/plugins/shortcuts/README.md @@ -17,23 +17,20 @@ import { Shortcuts } from '@backstage/plugin-shortcuts'; ; ``` -The plugin exports a `shortcutApiRef` and an implementation of the `ShortcutApi` that uses `localStorage` for storage, that you can use to get started quickly. To use it add it to your app's `apis.ts`: +The plugin exports a `shortcutApiRef` but the plugin includes a default implementation of the `ShortcutApi` that uses `localStorage` to store each users shortcuts. + +To overwrite the default implementation add it to the App's `apis.ts`: ```ts -import { - LocalStoredShortcuts, - shortcutsApiRef, -} from '@backstage/plugin-shortcuts'; +import { shortcutsApiRef } from '@backstage/plugin-shortcuts'; +import { CustomShortcutsImpl } from '...'; export const apis = [ // ... createApiFactory({ api: shortcutsApiRef, - deps: { errorApi: errorApiRef }, - factory: ({ errorApi }) => - new LocalStoredShortcuts( - WebStorage.create({ namespace: '@backstage/shortcuts', errorApi }), - ), + deps: {}, + factory: () => new CustomShortcutsImpl(), }), ]; ``` diff --git a/plugins/shortcuts/src/plugin.ts b/plugins/shortcuts/src/plugin.ts index b81062505a..e2cd819fc6 100644 --- a/plugins/shortcuts/src/plugin.ts +++ b/plugins/shortcuts/src/plugin.ts @@ -14,10 +14,24 @@ * limitations under the License. */ -import { createComponentExtension, createPlugin } from '@backstage/core'; +import { + createApiFactory, + createComponentExtension, + createPlugin, + storageApiRef, +} from '@backstage/core'; +import { LocalStoredShortcuts, shortcutsApiRef } from './api'; export const shortcutsPlugin = createPlugin({ id: 'shortcuts', + apis: [ + createApiFactory({ + api: shortcutsApiRef, + deps: { storageApi: storageApiRef }, + factory: ({ storageApi }) => + new LocalStoredShortcuts(storageApi.forBucket('shortcuts')), + }), + ], }); export const Shortcuts = shortcutsPlugin.provide( From de6c739401316dcbba4a38239a1dbd6b41507ced Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Fri, 30 Apr 2021 09:53:30 +0200 Subject: [PATCH 17/27] Remove by id only Signed-off-by: Marcus Eide --- plugins/shortcuts/src/EditShortcut.test.tsx | 6 +----- plugins/shortcuts/src/EditShortcut.tsx | 2 +- plugins/shortcuts/src/api/LocalStoredShortcuts.ts | 4 ++-- plugins/shortcuts/src/api/ShortcutApi.ts | 2 +- 4 files changed, 5 insertions(+), 9 deletions(-) diff --git a/plugins/shortcuts/src/EditShortcut.test.tsx b/plugins/shortcuts/src/EditShortcut.test.tsx index bf2396e194..20ef433c37 100644 --- a/plugins/shortcuts/src/EditShortcut.test.tsx +++ b/plugins/shortcuts/src/EditShortcut.test.tsx @@ -86,11 +86,7 @@ describe('EditShortcut', () => { fireEvent.click(screen.getByText('Remove')); await waitFor(() => { - expect(spy).toBeCalledWith({ - id: 'id', - title: 'some title', - url: '/some-url', - }); + expect(spy).toBeCalledWith('id'); }); }); diff --git a/plugins/shortcuts/src/EditShortcut.tsx b/plugins/shortcuts/src/EditShortcut.tsx index 8b98406703..efe8726992 100644 --- a/plugins/shortcuts/src/EditShortcut.tsx +++ b/plugins/shortcuts/src/EditShortcut.tsx @@ -78,7 +78,7 @@ export const EditShortcut = ({ shortcut, onClose, anchorEl, api }: Props) => { const handleRemove = async () => { try { - await api.remove(shortcut); + await api.remove(shortcut.id); alertApi.post({ message: `Removed shortcut '${shortcut.title}' from your sidebar`, severity: 'success', diff --git a/plugins/shortcuts/src/api/LocalStoredShortcuts.ts b/plugins/shortcuts/src/api/LocalStoredShortcuts.ts index 377f8e9190..ee8735edb4 100644 --- a/plugins/shortcuts/src/api/LocalStoredShortcuts.ts +++ b/plugins/shortcuts/src/api/LocalStoredShortcuts.ts @@ -39,8 +39,8 @@ export class LocalStoredShortcuts implements ShortcutApi { this.notify(); } - async remove(shortcut: Shortcut) { - const shortcuts = this.get().filter(s => s.id !== shortcut.id); + async remove(id: string) { + const shortcuts = this.get().filter(s => s.id !== id); await this.storageApi.set('items', shortcuts); this.notify(); diff --git a/plugins/shortcuts/src/api/ShortcutApi.ts b/plugins/shortcuts/src/api/ShortcutApi.ts index 826988d01f..74b6aaf777 100644 --- a/plugins/shortcuts/src/api/ShortcutApi.ts +++ b/plugins/shortcuts/src/api/ShortcutApi.ts @@ -36,7 +36,7 @@ export interface ShortcutApi { /** * Removes the shortcut. */ - remove(shortcut: Shortcut): Promise; + remove(id: string): Promise; /** * Finds an existing shortcut that matches the ID of the From de2afb8621494b0f0f9e8fd438792d2eb30bf8b0 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Fri, 30 Apr 2021 13:52:36 +0200 Subject: [PATCH 18/27] Use :hover instead of event listeners to display edit button Signed-off-by: Marcus Eide --- plugins/shortcuts/src/ShortcutItem.test.tsx | 20 ----------- plugins/shortcuts/src/ShortcutItem.tsx | 38 +++++++++++---------- 2 files changed, 20 insertions(+), 38 deletions(-) diff --git a/plugins/shortcuts/src/ShortcutItem.test.tsx b/plugins/shortcuts/src/ShortcutItem.test.tsx index 219771909d..d32ec3d392 100644 --- a/plugins/shortcuts/src/ShortcutItem.test.tsx +++ b/plugins/shortcuts/src/ShortcutItem.test.tsx @@ -81,26 +81,6 @@ describe('ShortcutItem', () => { }); }); - it('displays the edit icon on hover', async () => { - render( - wrapInTestApp( - - - , - ), - ); - - fireEvent.mouseOver(screen.getByText('ST')); - await waitFor(() => { - expect(screen.getByTestId('edit')).toBeInTheDocument(); - }); - - fireEvent.mouseOut(screen.getByText('ST')); - await waitFor(() => { - expect(screen.queryByTestId('edit')).not.toBeInTheDocument(); - }); - }); - it('gets the color based on the theme', async () => { const { rerender } = render( wrapInTestApp(), diff --git a/plugins/shortcuts/src/ShortcutItem.tsx b/plugins/shortcuts/src/ShortcutItem.tsx index 3aec4bb3b4..f9868182f0 100644 --- a/plugins/shortcuts/src/ShortcutItem.tsx +++ b/plugins/shortcuts/src/ShortcutItem.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { useState } from 'react'; +import React from 'react'; import { SidebarItem } from '@backstage/core'; import { IconButton, makeStyles } from '@material-ui/core'; import EditIcon from '@material-ui/icons/Edit'; @@ -24,6 +24,14 @@ import { ShortcutApi } from './api'; import { Shortcut } from './types'; const useStyles = makeStyles({ + root: { + '&:hover #edit': { + visibility: 'visible', + }, + }, + button: { + visibility: 'hidden', + }, icon: { color: 'white', fontSize: 16, @@ -50,7 +58,6 @@ type Props = { export const ShortcutItem = ({ shortcut, api }: Props) => { const classes = useStyles(); - const [displayEdit, setDisplayEdit] = useState(false); const [anchorEl, setAnchorEl] = React.useState(); const handleClick = (event: React.MouseEvent) => { @@ -60,32 +67,27 @@ export const ShortcutItem = ({ shortcut, api }: Props) => { 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 ( -
+ <> } > - {displayEdit && ( - - - - )} + + + { api={api} shortcut={shortcut} /> -
+ ); }; From cea80105ec1627d72148e6ed8a2b1da17851af6d Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Fri, 30 Apr 2021 15:25:51 +0200 Subject: [PATCH 19/27] Fix test and unused import Signed-off-by: Marcus Eide --- plugins/shortcuts/src/ShortcutItem.test.tsx | 2 +- plugins/shortcuts/src/api/LocalStoredShortcuts.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/shortcuts/src/ShortcutItem.test.tsx b/plugins/shortcuts/src/ShortcutItem.test.tsx index d32ec3d392..ffeec0cdc4 100644 --- a/plugins/shortcuts/src/ShortcutItem.test.tsx +++ b/plugins/shortcuts/src/ShortcutItem.test.tsx @@ -15,7 +15,7 @@ */ import React from 'react'; -import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { render, screen, waitFor } from '@testing-library/react'; import { ShortcutItem } from './ShortcutItem'; import { Shortcut } from './types'; import { SidebarContext } from '@backstage/core'; diff --git a/plugins/shortcuts/src/api/LocalStoredShortcuts.test.ts b/plugins/shortcuts/src/api/LocalStoredShortcuts.test.ts index 94f63f846a..7ca457fd39 100644 --- a/plugins/shortcuts/src/api/LocalStoredShortcuts.test.ts +++ b/plugins/shortcuts/src/api/LocalStoredShortcuts.test.ts @@ -69,7 +69,7 @@ describe('LocalStoredShortcuts', () => { const shortcut: Shortcut = { id: 'someid', title: 'title', url: '/url' }; const spy = jest.spyOn(storageApi, 'set'); - await shortcutApi.remove(shortcut); + await shortcutApi.remove(shortcut.id); expect(spy).toHaveBeenCalledWith('items', []); }); From 3796235816251ef9d3bc7907f5fae877e30d52ef Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Fri, 30 Apr 2021 17:18:07 +0200 Subject: [PATCH 20/27] Update deps versions Signed-off-by: Marcus Eide --- plugins/shortcuts/package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/shortcuts/package.json b/plugins/shortcuts/package.json index b3d8d7f4c1..0bc80fa180 100644 --- a/plugins/shortcuts/package.json +++ b/plugins/shortcuts/package.json @@ -20,8 +20,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.7.5", - "@backstage/theme": "^0.2.5", + "@backstage/core": "^0.7.7", + "@backstage/theme": "^0.2.6", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -35,7 +35,7 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.6.8", + "@backstage/cli": "^0.6.9", "@backstage/dev-utils": "^0.1.13", "@backstage/test-utils": "^0.1.10", "@testing-library/jest-dom": "^5.10.1", From 7c9d8c3b187626e156367112dcc523b494c5845e Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Thu, 6 May 2021 10:17:47 +0200 Subject: [PATCH 21/27] Update README Signed-off-by: Marcus Eide --- plugins/shortcuts/README.md | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/plugins/shortcuts/README.md b/plugins/shortcuts/README.md index dfe8577190..36bd9e62f8 100644 --- a/plugins/shortcuts/README.md +++ b/plugins/shortcuts/README.md @@ -1,12 +1,26 @@ # shortcuts -`shortcuts-plugin` allows a user to have easy access to pages within a Backstage app by storing them as "shortcuts" in the Sidebar. +The shortcuts plugin allows a user to have easy access to pages within a Backstage app by storing them as "shortcuts" in the Sidebar. ## Usage +Install the package: + +```bash +yarn add @backstage/plugin-shortcuts +``` + +Add it to your App's `plugins.ts` file: + +```ts +// ... +export { shortcutsPlugin } from '@backstage/plugin-shortcuts'; +``` + Add the `` component within your ``: ```ts +import { Sidebar, SidebarDivider, SidebarSpace } from '@backstage/core'; import { Shortcuts } from '@backstage/plugin-shortcuts'; @@ -17,7 +31,7 @@ import { Shortcuts } from '@backstage/plugin-shortcuts'; ; ``` -The plugin exports a `shortcutApiRef` but the plugin includes a default implementation of the `ShortcutApi` that uses `localStorage` to store each users shortcuts. +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`: From 5d2d8e505c535cb5922106d0666cf8666fc42517 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Thu, 6 May 2021 10:25:38 +0200 Subject: [PATCH 22/27] Use renderInTestApp in test where possible Signed-off-by: Marcus Eide --- plugins/shortcuts/src/AddShortcut.test.tsx | 38 +++++++---------- plugins/shortcuts/src/EditShortcut.test.tsx | 36 +++++++---------- plugins/shortcuts/src/ShortcutForm.test.tsx | 20 +++++---- plugins/shortcuts/src/ShortcutItem.test.tsx | 45 +++++++++------------ plugins/shortcuts/src/Shortcuts.test.tsx | 18 ++++----- 5 files changed, 66 insertions(+), 91 deletions(-) diff --git a/plugins/shortcuts/src/AddShortcut.test.tsx b/plugins/shortcuts/src/AddShortcut.test.tsx index cee5d8cf28..8001cdc293 100644 --- a/plugins/shortcuts/src/AddShortcut.test.tsx +++ b/plugins/shortcuts/src/AddShortcut.test.tsx @@ -15,10 +15,10 @@ */ import React from 'react'; -import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { screen, fireEvent, waitFor } from '@testing-library/react'; import { AddShortcut } from './AddShortcut'; import { LocalStoredShortcuts } from './api'; -import { MockStorageApi, wrapInTestApp } from '@backstage/test-utils'; +import { MockStorageApi, renderInTestApp } from '@backstage/test-utils'; import { AlertDisplay } from '@backstage/core'; describe('AddShortcut', () => { @@ -36,26 +36,22 @@ describe('AddShortcut', () => { }); it('displays the title', async () => { - render(wrapInTestApp()); + await renderInTestApp(); - await waitFor(() => { - expect(screen.getByText('Add Shortcut')).toBeInTheDocument(); - }); + expect(screen.getByText('Add Shortcut')).toBeInTheDocument(); }); it('closes the popup', async () => { - render(wrapInTestApp()); + await renderInTestApp(); fireEvent.click(screen.getByText('Cancel')); - await waitFor(() => { - expect(props.onClose).toHaveBeenCalledTimes(1); - }); + expect(props.onClose).toHaveBeenCalledTimes(1); }); it('saves the input', async () => { const spy = jest.spyOn(api, 'add'); - render(wrapInTestApp()); + await renderInTestApp(); const urlInput = screen.getByPlaceholderText('Enter a URL'); const titleInput = screen.getByPlaceholderText('Enter a display name'); @@ -74,11 +70,9 @@ describe('AddShortcut', () => { it('pastes the values', async () => { const spy = jest.spyOn(api, 'add'); - render( - wrapInTestApp(, { - routeEntries: ['/some-initial-url'], - }), - ); + await renderInTestApp(, { + routeEntries: ['/some-initial-url'], + }); fireEvent.click(screen.getByText('Use current page')); fireEvent.click(screen.getByText('Save')); @@ -93,13 +87,11 @@ describe('AddShortcut', () => { it('displays errors', async () => { jest.spyOn(api, 'add').mockRejectedValueOnce(new Error('some add error')); - render( - wrapInTestApp( - <> - - - , - ), + await renderInTestApp( + <> + + + , ); fireEvent.click(screen.getByText('Use current page')); diff --git a/plugins/shortcuts/src/EditShortcut.test.tsx b/plugins/shortcuts/src/EditShortcut.test.tsx index 20ef433c37..eaab6cf609 100644 --- a/plugins/shortcuts/src/EditShortcut.test.tsx +++ b/plugins/shortcuts/src/EditShortcut.test.tsx @@ -15,11 +15,11 @@ */ import React from 'react'; -import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { screen, fireEvent, waitFor } from '@testing-library/react'; import { EditShortcut } from './EditShortcut'; import { Shortcut } from './types'; import { LocalStoredShortcuts } from './api'; -import { MockStorageApi, wrapInTestApp } from '@backstage/test-utils'; +import { MockStorageApi, renderInTestApp } from '@backstage/test-utils'; import { AlertDisplay } from '@backstage/core'; describe('EditShortcut', () => { @@ -42,26 +42,22 @@ describe('EditShortcut', () => { }); it('displays the title', async () => { - render(wrapInTestApp()); + await renderInTestApp(); - await waitFor(() => { - expect(screen.getByText('Edit Shortcut')).toBeInTheDocument(); - }); + expect(screen.getByText('Edit Shortcut')).toBeInTheDocument(); }); it('closes the popup', async () => { - render(wrapInTestApp()); + await renderInTestApp(); fireEvent.click(screen.getByText('Cancel')); - await waitFor(() => { - expect(props.onClose).toHaveBeenCalledTimes(1); - }); + expect(props.onClose).toHaveBeenCalledTimes(1); }); it('updates the shortcut', async () => { const spy = jest.spyOn(api, 'update'); - render(wrapInTestApp()); + await renderInTestApp(); const urlInput = screen.getByPlaceholderText('Enter a URL'); const titleInput = screen.getByPlaceholderText('Enter a display name'); @@ -82,12 +78,10 @@ describe('EditShortcut', () => { it('removes the shortcut', async () => { const spy = jest.spyOn(api, 'remove'); - render(wrapInTestApp()); + await renderInTestApp(); fireEvent.click(screen.getByText('Remove')); - await waitFor(() => { - expect(spy).toBeCalledWith('id'); - }); + expect(spy).toBeCalledWith('id'); }); it('displays errors', async () => { @@ -99,13 +93,11 @@ describe('EditShortcut', () => { .spyOn(api, 'remove') .mockRejectedValueOnce(new Error('some remove error')); - render( - wrapInTestApp( - <> - - - , - ), + await renderInTestApp( + <> + + + , ); fireEvent.click(screen.getByText('Save')); diff --git a/plugins/shortcuts/src/ShortcutForm.test.tsx b/plugins/shortcuts/src/ShortcutForm.test.tsx index 4586c484d0..23a36126c6 100644 --- a/plugins/shortcuts/src/ShortcutForm.test.tsx +++ b/plugins/shortcuts/src/ShortcutForm.test.tsx @@ -14,9 +14,9 @@ * limitations under the License. */ import React from 'react'; -import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { screen, fireEvent, waitFor } from '@testing-library/react'; import { ShortcutForm } from './ShortcutForm'; -import { wrapInTestApp } from '@backstage/test-utils'; +import { renderInTestApp } from '@backstage/test-utils'; describe('ShortcutForm', () => { const props = { @@ -25,7 +25,7 @@ describe('ShortcutForm', () => { }; it('displays validation messages', async () => { - render(wrapInTestApp()); + await renderInTestApp(); const urlInput = screen.getByPlaceholderText('Enter a URL'); const titleInput = screen.getByPlaceholderText('Enter a display name'); @@ -44,13 +44,11 @@ describe('ShortcutForm', () => { }); it('calls the save handler', async () => { - render( - wrapInTestApp( - , - ), + await renderInTestApp( + , ); fireEvent.click(screen.getByText('Save')); @@ -63,7 +61,7 @@ describe('ShortcutForm', () => { }); it('calls the close handler', async () => { - render(wrapInTestApp()); + await renderInTestApp(); fireEvent.click(screen.getByText('Cancel')); await waitFor(() => { diff --git a/plugins/shortcuts/src/ShortcutItem.test.tsx b/plugins/shortcuts/src/ShortcutItem.test.tsx index ffeec0cdc4..080f847952 100644 --- a/plugins/shortcuts/src/ShortcutItem.test.tsx +++ b/plugins/shortcuts/src/ShortcutItem.test.tsx @@ -15,12 +15,16 @@ */ import React from 'react'; -import { render, screen, waitFor } from '@testing-library/react'; +import { screen, waitFor } from '@testing-library/react'; import { ShortcutItem } from './ShortcutItem'; import { Shortcut } from './types'; import { SidebarContext } from '@backstage/core'; import { LocalStoredShortcuts } from './api'; -import { MockStorageApi, wrapInTestApp } from '@backstage/test-utils'; +import { + MockStorageApi, + renderInTestApp, + wrapInTestApp, +} from '@backstage/test-utils'; import { pageTheme } from '@backstage/theme'; describe('ShortcutItem', () => { @@ -32,17 +36,13 @@ describe('ShortcutItem', () => { const api = new LocalStoredShortcuts(MockStorageApi.create()); it('displays the shortcut', async () => { - render( - wrapInTestApp( - - - , - ), + await renderInTestApp( + + + , ); - await waitFor(() => { - expect(screen.getByText('ST')).toBeInTheDocument(); - expect(screen.getByText('some title')).toBeInTheDocument(); - }); + expect(screen.getByText('ST')).toBeInTheDocument(); + expect(screen.getByText('some title')).toBeInTheDocument(); }); it('calculates the shortcut text correctly', async () => { @@ -62,13 +62,10 @@ describe('ShortcutItem', () => { title: 'more | title words', }; - const { rerender } = render( - wrapInTestApp(), + const { rerender } = await renderInTestApp( + , ); - - await waitFor(() => { - expect(screen.getByText('On')).toBeInTheDocument(); - }); + expect(screen.getByText('On')).toBeInTheDocument(); rerender(wrapInTestApp()); await waitFor(() => { @@ -82,15 +79,13 @@ describe('ShortcutItem', () => { }); it('gets the color based on the theme', async () => { - const { rerender } = render( - wrapInTestApp(), + const { rerender } = await renderInTestApp( + , ); - await waitFor(() => { - expect(document.querySelector('circle')?.getAttribute('fill')).toEqual( - pageTheme.tool.colors[0], - ); - }); + expect(document.querySelector('circle')?.getAttribute('fill')).toEqual( + pageTheme.tool.colors[0], + ); const newShortcut: Shortcut = { id: 'id', diff --git a/plugins/shortcuts/src/Shortcuts.test.tsx b/plugins/shortcuts/src/Shortcuts.test.tsx index 088a3705d9..b697f9ed27 100644 --- a/plugins/shortcuts/src/Shortcuts.test.tsx +++ b/plugins/shortcuts/src/Shortcuts.test.tsx @@ -16,8 +16,8 @@ import React from 'react'; import { SidebarContext, ApiProvider, ApiRegistry } from '@backstage/core'; -import { wrapInTestApp, MockStorageApi } from '@backstage/test-utils'; -import { render, screen, waitFor } from '@testing-library/react'; +import { MockStorageApi, renderInTestApp } from '@backstage/test-utils'; +import { screen, waitFor } from '@testing-library/react'; import { Shortcuts } from './Shortcuts'; import { LocalStoredShortcuts, shortcutsApiRef } from './api'; @@ -27,14 +27,12 @@ const apis = ApiRegistry.from([ describe('Shortcuts', () => { it('displays an add button', async () => { - render( - wrapInTestApp( - - - - - , - ), + await renderInTestApp( + + + + + , ); await waitFor(() => !screen.queryByTestId('progress')); expect(screen.getByText('Add Shortcuts')).toBeInTheDocument(); From 93ebf0ef2426dced9ff1974815f56c8b77e31309 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Thu, 6 May 2021 10:26:11 +0200 Subject: [PATCH 23/27] Change exports Signed-off-by: Marcus Eide --- plugins/shortcuts/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/shortcuts/src/index.ts b/plugins/shortcuts/src/index.ts index 851178710f..70f874e005 100644 --- a/plugins/shortcuts/src/index.ts +++ b/plugins/shortcuts/src/index.ts @@ -15,4 +15,4 @@ */ export { shortcutsPlugin, Shortcuts } from './plugin'; export * from './api'; -export * from './types'; +export type { Shortcut } from './types'; From 1582d4c7be4cdd55efe23de146d162904b6b06e6 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Thu, 6 May 2021 14:31:19 +0200 Subject: [PATCH 24/27] Correct backstage/core version Signed-off-by: Marcus Eide --- plugins/shortcuts/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/shortcuts/package.json b/plugins/shortcuts/package.json index 0bc80fa180..c65d51143f 100644 --- a/plugins/shortcuts/package.json +++ b/plugins/shortcuts/package.json @@ -20,7 +20,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.7.7", + "@backstage/core": "^0.7.6", "@backstage/theme": "^0.2.6", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", From bcf4c2f91d6c65505d961f4830c3952a42d74929 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Fri, 7 May 2021 11:20:29 +0200 Subject: [PATCH 25/27] More package versions Signed-off-by: Marcus Eide --- plugins/shortcuts/package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/shortcuts/package.json b/plugins/shortcuts/package.json index c65d51143f..7c60f54485 100644 --- a/plugins/shortcuts/package.json +++ b/plugins/shortcuts/package.json @@ -20,8 +20,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.7.6", - "@backstage/theme": "^0.2.6", + "@backstage/core": "^0.7.8", + "@backstage/theme": "^0.2.7", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -35,7 +35,7 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.6.9", + "@backstage/cli": "^0.6.10", "@backstage/dev-utils": "^0.1.13", "@backstage/test-utils": "^0.1.10", "@testing-library/jest-dom": "^5.10.1", From 3b269181b68625f283269cdca9200da3a3937b3b Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Fri, 7 May 2021 16:01:34 +0200 Subject: [PATCH 26/27] Update README with tsx code block Signed-off-by: Marcus Eide --- plugins/shortcuts/README.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/plugins/shortcuts/README.md b/plugins/shortcuts/README.md index 36bd9e62f8..162afe009b 100644 --- a/plugins/shortcuts/README.md +++ b/plugins/shortcuts/README.md @@ -19,16 +19,18 @@ export { shortcutsPlugin } from '@backstage/plugin-shortcuts'; Add the `` component within your ``: -```ts +```tsx import { Sidebar, SidebarDivider, SidebarSpace } from '@backstage/core'; import { Shortcuts } from '@backstage/plugin-shortcuts'; - - {/* ... */} - - - -; +export const SidebarComponent = () => ( + + {/* ... */} + + + + +); ``` The plugin exports a `shortcutApiRef` but the plugin includes a default implementation of the `ShortcutApi` that uses `localStorage` to store each user's shortcuts. From afb968c02170669d05d1a8b55f9d86436770c113 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Fri, 7 May 2021 17:09:41 +0200 Subject: [PATCH 27/27] Update react-use version Signed-off-by: Marcus Eide --- plugins/shortcuts/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/shortcuts/package.json b/plugins/shortcuts/package.json index 7c60f54485..f87fe0e683 100644 --- a/plugins/shortcuts/package.json +++ b/plugins/shortcuts/package.json @@ -30,7 +30,7 @@ "react-dom": "^16.13.1", "react-hook-form": "^7.1.1", "react-router": "6.0.0-beta.0", - "react-use": "^15.3.3", + "react-use": "^17.2.4", "uuid": "^8.3.2", "zen-observable": "^0.8.15" },