) => {
+ 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==