diff --git a/.changeset/violet-trees-play.md b/.changeset/violet-trees-play.md new file mode 100644 index 0000000000..00f1e156a8 --- /dev/null +++ b/.changeset/violet-trees-play.md @@ -0,0 +1,14 @@ +--- +'@backstage/plugin-shortcuts': minor +--- + +Internal observable replaced with a mapping from the storage API. This fixes shortcuts initialization when using firestore. + +`ShortcutApi.get` method, that returns an immediate snapshot of shortcuts, made public. + +Example of how to get and observe `shortcuts`: + +```typescript +const shortcutApi = useApi(shortcutsApiRef); +const shortcuts = useObservable(shortcutApi.shortcut$(), shortcutApi.get()); +``` diff --git a/plugins/shortcuts/api-report.md b/plugins/shortcuts/api-report.md index 537ce972ab..7f81df835d 100644 --- a/plugins/shortcuts/api-report.md +++ b/plugins/shortcuts/api-report.md @@ -9,7 +9,7 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { IconComponent } from '@backstage/core-plugin-api'; import { Observable } from '@backstage/types'; -import ObservableImpl from 'zen-observable'; +import { default as Observable_2 } from 'zen-observable'; import { StorageApi } from '@backstage/core-plugin-api'; // Warning: (ae-missing-release-tag) "LocalStoredShortcuts" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -20,11 +20,13 @@ export class LocalStoredShortcuts implements ShortcutApi { // (undocumented) add(shortcut: Omit): Promise; // (undocumented) + get(): Shortcut[]; + // (undocumented) getColor(url: string): string; // (undocumented) remove(id: string): Promise; // (undocumented) - shortcut$(): ObservableImpl; + shortcut$(): Observable_2; // (undocumented) update(shortcut: Shortcut): Promise; } @@ -43,6 +45,7 @@ export type Shortcut = { // @public (undocumented) export interface ShortcutApi { add(shortcut: Omit): Promise; + get(): Shortcut[]; getColor(url: string): string; remove(id: string): Promise; shortcut$(): Observable; diff --git a/plugins/shortcuts/src/Shortcuts.tsx b/plugins/shortcuts/src/Shortcuts.tsx index b8af1f2e29..28aa33e8f2 100644 --- a/plugins/shortcuts/src/Shortcuts.tsx +++ b/plugins/shortcuts/src/Shortcuts.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { useMemo } from 'react'; +import React from 'react'; import useObservable from 'react-use/lib/useObservable'; import PlayListAddIcon from '@material-ui/icons/PlaylistAdd'; import { ShortcutItem } from './ShortcutItem'; @@ -39,9 +39,7 @@ export interface ShortcutsProps { export const Shortcuts = (props: ShortcutsProps) => { const shortcutApi = useApi(shortcutsApiRef); - const shortcuts = useObservable( - useMemo(() => shortcutApi.shortcut$(), [shortcutApi]), - ); + const shortcuts = useObservable(shortcutApi.shortcut$(), shortcutApi.get()); 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 c8401e8b1a..dbea91863c 100644 --- a/plugins/shortcuts/src/api/LocalStoredShortcuts.test.ts +++ b/plugins/shortcuts/src/api/LocalStoredShortcuts.test.ts @@ -27,17 +27,22 @@ describe('LocalStoredShortcuts', () => { ); const shortcut: Shortcut = { id: 'id', title: 'title', url: '/url' }; - await shortcutApi.add(shortcut); + const observerNextHandler = jest.fn(); await new Promise(resolve => { - const subscription = shortcutApi.shortcut$().subscribe(data => { - expect(data).toEqual( - expect.arrayContaining([{ ...shortcut, id: expect.anything() }]), - ); - subscription.unsubscribe(); - resolve(); + shortcutApi.shortcut$().subscribe({ + next: data => { + observerNextHandler(data); + resolve(); + }, }); + shortcutApi.add(shortcut); }); + + expect(observerNextHandler).toHaveBeenCalledTimes(1); + expect(observerNextHandler).toHaveBeenCalledWith( + expect.arrayContaining([{ ...shortcut, id: expect.anything() }]), + ); }); it('should add shortcuts with ids', async () => { diff --git a/plugins/shortcuts/src/api/LocalStoredShortcuts.ts b/plugins/shortcuts/src/api/LocalStoredShortcuts.ts index ef54f68acd..82f4b7dd1a 100644 --- a/plugins/shortcuts/src/api/LocalStoredShortcuts.ts +++ b/plugins/shortcuts/src/api/LocalStoredShortcuts.ts @@ -15,11 +15,11 @@ */ import { pageTheme } from '@backstage/theme'; -import ObservableImpl from 'zen-observable'; import { v4 as uuid } from 'uuid'; import { ShortcutApi } from './ShortcutApi'; import { Shortcut } from '../types'; import { StorageApi } from '@backstage/core-plugin-api'; +import Observable from 'zen-observable'; /** * Implementation of the ShortcutApi that uses the StorageApi to store shortcuts. @@ -28,7 +28,15 @@ export class LocalStoredShortcuts implements ShortcutApi { constructor(private readonly storageApi: StorageApi) {} shortcut$() { - return this.observable; + return Observable.from(this.storageApi.observe$('items')).map( + snapshot => snapshot.value ?? [], + ); + } + + get() { + return Array.from( + this.storageApi.snapshot('items').value ?? [], + ).sort((a, b) => (a.title >= b.title ? 1 : -1)); } async add(shortcut: Omit) { @@ -36,14 +44,12 @@ export class LocalStoredShortcuts implements ShortcutApi { shortcuts.push({ ...shortcut, id: uuid() }); await this.storageApi.set('items', shortcuts); - this.notify(); } async remove(id: string) { const shortcuts = this.get().filter(s => s.id !== id); await this.storageApi.set('items', shortcuts); - this.notify(); } async update(shortcut: Shortcut) { @@ -51,7 +57,6 @@ export class LocalStoredShortcuts implements ShortcutApi { shortcuts.push(shortcut); await this.storageApi.set('items', shortcuts); - this.notify(); } getColor(url: string) { @@ -63,33 +68,8 @@ export class LocalStoredShortcuts implements ShortcutApi { 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 Array.from( - this.storageApi.snapshot('items').value ?? [], - ).sort((a, b) => (a.title >= b.title ? 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 index 893a2fd74b..1f93baa66c 100644 --- a/plugins/shortcuts/src/api/ShortcutApi.ts +++ b/plugins/shortcuts/src/api/ShortcutApi.ts @@ -28,6 +28,11 @@ export interface ShortcutApi { */ shortcut$(): Observable; + /** + * Returns an immediate snapshot of shortcuts, sorted by title + */ + get(): Shortcut[]; + /** * Generates a unique id for the shortcut and then saves it. */