From 5b769fddb5d11be117aa9acdec765486d9e3a393 Mon Sep 17 00:00:00 2001 From: Alex Rybchenko Date: Mon, 11 Jul 2022 17:32:47 +0200 Subject: [PATCH 1/6] added initial storage api subscription Signed-off-by: Alex Rybchenko --- .changeset/violet-trees-play.md | 5 +++++ plugins/shortcuts/src/api/LocalStoredShortcuts.ts | 9 +++++---- 2 files changed, 10 insertions(+), 4 deletions(-) create mode 100644 .changeset/violet-trees-play.md diff --git a/.changeset/violet-trees-play.md b/.changeset/violet-trees-play.md new file mode 100644 index 0000000000..c44c75e98d --- /dev/null +++ b/.changeset/violet-trees-play.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-shortcuts': patch +--- + +fixed shortcuts initialization when using firestore diff --git a/plugins/shortcuts/src/api/LocalStoredShortcuts.ts b/plugins/shortcuts/src/api/LocalStoredShortcuts.ts index ef54f68acd..884f561aaa 100644 --- a/plugins/shortcuts/src/api/LocalStoredShortcuts.ts +++ b/plugins/shortcuts/src/api/LocalStoredShortcuts.ts @@ -25,7 +25,11 @@ import { StorageApi } from '@backstage/core-plugin-api'; * Implementation of the ShortcutApi that uses the StorageApi to store shortcuts. */ export class LocalStoredShortcuts implements ShortcutApi { - constructor(private readonly storageApi: StorageApi) {} + constructor(private readonly storageApi: StorageApi) { + this.storageApi.observe$('items').subscribe({ + next: () => this.notify(), + }); + } shortcut$() { return this.observable; @@ -36,14 +40,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 +53,6 @@ export class LocalStoredShortcuts implements ShortcutApi { shortcuts.push(shortcut); await this.storageApi.set('items', shortcuts); - this.notify(); } getColor(url: string) { From 2e17ad5e30d26b00f7dc1844784224e0b1b0b4f3 Mon Sep 17 00:00:00 2001 From: Alex Rybchenko Date: Wed, 13 Jul 2022 16:17:54 +0200 Subject: [PATCH 2/6] replaced internal observable with a mapping from the storage API Signed-off-by: Alex Rybchenko --- plugins/shortcuts/src/Shortcuts.tsx | 3 +- .../src/api/LocalStoredShortcuts.test.ts | 19 ++++--- .../shortcuts/src/api/LocalStoredShortcuts.ts | 49 ++++++------------- plugins/shortcuts/src/api/ShortcutApi.ts | 5 ++ 4 files changed, 33 insertions(+), 43 deletions(-) diff --git a/plugins/shortcuts/src/Shortcuts.tsx b/plugins/shortcuts/src/Shortcuts.tsx index b8af1f2e29..3c48e2bae2 100644 --- a/plugins/shortcuts/src/Shortcuts.tsx +++ b/plugins/shortcuts/src/Shortcuts.tsx @@ -40,7 +40,8 @@ export interface ShortcutsProps { export const Shortcuts = (props: ShortcutsProps) => { const shortcutApi = useApi(shortcutsApiRef); const shortcuts = useObservable( - useMemo(() => shortcutApi.shortcut$(), [shortcutApi]), + shortcutApi.shortcut$(), + shortcutApi.snapshot(), ); 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 884f561aaa..ba3722da66 100644 --- a/plugins/shortcuts/src/api/LocalStoredShortcuts.ts +++ b/plugins/shortcuts/src/api/LocalStoredShortcuts.ts @@ -15,41 +15,45 @@ */ 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. */ export class LocalStoredShortcuts implements ShortcutApi { - constructor(private readonly storageApi: StorageApi) { - this.storageApi.observe$('items').subscribe({ - next: () => this.notify(), - }); - } + constructor(private readonly storageApi: StorageApi) {} shortcut$() { - return this.observable; + return Observable.from(this.storageApi.observe$('items')).map( + snapshot => snapshot.value ?? [], + ); + } + + snapshot() { + return Array.from( + this.storageApi.snapshot('items').value ?? [], + ).sort((a, b) => (a.title >= b.title ? 1 : -1)); } async add(shortcut: Omit) { - const shortcuts = this.get(); + const shortcuts = this.snapshot(); shortcuts.push({ ...shortcut, id: uuid() }); await this.storageApi.set('items', shortcuts); } async remove(id: string) { - const shortcuts = this.get().filter(s => s.id !== id); + const shortcuts = this.snapshot().filter(s => s.id !== id); await this.storageApi.set('items', shortcuts); } async update(shortcut: Shortcut) { - const shortcuts = this.get().filter(s => s.id !== shortcut.id); + const shortcuts = this.snapshot().filter(s => s.id !== shortcut.id); shortcuts.push(shortcut); await this.storageApi.set('items', shortcuts); @@ -64,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..542e77bac8 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 + */ + snapshot(): Shortcut[]; + /** * Generates a unique id for the shortcut and then saves it. */ From 4919b842d890b49f542e7ffe22991c334d8242c4 Mon Sep 17 00:00:00 2001 From: Alex Rybchenko Date: Wed, 13 Jul 2022 16:44:26 +0200 Subject: [PATCH 3/6] removed unused import Signed-off-by: Alex Rybchenko --- plugins/shortcuts/src/Shortcuts.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/shortcuts/src/Shortcuts.tsx b/plugins/shortcuts/src/Shortcuts.tsx index 3c48e2bae2..75baf5fd96 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'; From 5416c9af063ddfea85c4b65ab7dfcd3cdbf62444 Mon Sep 17 00:00:00 2001 From: Alex Rybchenko Date: Wed, 13 Jul 2022 17:10:28 +0200 Subject: [PATCH 4/6] updated api-report Signed-off-by: Alex Rybchenko --- plugins/shortcuts/api-report.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/plugins/shortcuts/api-report.md b/plugins/shortcuts/api-report.md index 4b5a8e74df..a58245e0fe 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) @@ -24,7 +24,9 @@ export class LocalStoredShortcuts implements ShortcutApi { // (undocumented) remove(id: string): Promise; // (undocumented) - shortcut$(): ObservableImpl; + shortcut$(): Observable_2; + // (undocumented) + snapshot(): Shortcut[]; // (undocumented) update(shortcut: Shortcut): Promise; } @@ -46,6 +48,7 @@ export interface ShortcutApi { getColor(url: string): string; remove(id: string): Promise; shortcut$(): Observable; + snapshot(): Shortcut[]; update(shortcut: Shortcut): Promise; } From a6407c0f82c2578c01454113d33ca9cd4d108e2f Mon Sep 17 00:00:00 2001 From: Alex Rybchenko Date: Tue, 19 Jul 2022 16:10:43 +0200 Subject: [PATCH 5/6] rename method Signed-off-by: Alex Rybchenko --- .changeset/violet-trees-play.md | 4 ++-- plugins/shortcuts/api-report.md | 6 +++--- plugins/shortcuts/src/Shortcuts.tsx | 5 +---- plugins/shortcuts/src/api/LocalStoredShortcuts.ts | 8 ++++---- plugins/shortcuts/src/api/ShortcutApi.ts | 2 +- 5 files changed, 11 insertions(+), 14 deletions(-) diff --git a/.changeset/violet-trees-play.md b/.changeset/violet-trees-play.md index c44c75e98d..c56635633c 100644 --- a/.changeset/violet-trees-play.md +++ b/.changeset/violet-trees-play.md @@ -1,5 +1,5 @@ --- -'@backstage/plugin-shortcuts': patch +'@backstage/plugin-shortcuts': minor --- -fixed shortcuts initialization when using firestore +Fixed shortcuts initialization when using firestore diff --git a/plugins/shortcuts/api-report.md b/plugins/shortcuts/api-report.md index a58245e0fe..35c37bf536 100644 --- a/plugins/shortcuts/api-report.md +++ b/plugins/shortcuts/api-report.md @@ -20,14 +20,14 @@ 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$(): Observable_2; // (undocumented) - snapshot(): Shortcut[]; - // (undocumented) update(shortcut: Shortcut): Promise; } @@ -45,10 +45,10 @@ export type Shortcut = { // @public (undocumented) export interface ShortcutApi { add(shortcut: Omit): Promise; + get(): Shortcut[]; getColor(url: string): string; remove(id: string): Promise; shortcut$(): Observable; - snapshot(): Shortcut[]; update(shortcut: Shortcut): Promise; } diff --git a/plugins/shortcuts/src/Shortcuts.tsx b/plugins/shortcuts/src/Shortcuts.tsx index 75baf5fd96..28aa33e8f2 100644 --- a/plugins/shortcuts/src/Shortcuts.tsx +++ b/plugins/shortcuts/src/Shortcuts.tsx @@ -39,10 +39,7 @@ export interface ShortcutsProps { export const Shortcuts = (props: ShortcutsProps) => { const shortcutApi = useApi(shortcutsApiRef); - const shortcuts = useObservable( - shortcutApi.shortcut$(), - shortcutApi.snapshot(), - ); + const shortcuts = useObservable(shortcutApi.shortcut$(), shortcutApi.get()); const [anchorEl, setAnchorEl] = React.useState(); const loading = Boolean(!shortcuts); diff --git a/plugins/shortcuts/src/api/LocalStoredShortcuts.ts b/plugins/shortcuts/src/api/LocalStoredShortcuts.ts index ba3722da66..82f4b7dd1a 100644 --- a/plugins/shortcuts/src/api/LocalStoredShortcuts.ts +++ b/plugins/shortcuts/src/api/LocalStoredShortcuts.ts @@ -33,27 +33,27 @@ export class LocalStoredShortcuts implements ShortcutApi { ); } - snapshot() { + get() { return Array.from( this.storageApi.snapshot('items').value ?? [], ).sort((a, b) => (a.title >= b.title ? 1 : -1)); } async add(shortcut: Omit) { - const shortcuts = this.snapshot(); + const shortcuts = this.get(); shortcuts.push({ ...shortcut, id: uuid() }); await this.storageApi.set('items', shortcuts); } async remove(id: string) { - const shortcuts = this.snapshot().filter(s => s.id !== id); + const shortcuts = this.get().filter(s => s.id !== id); await this.storageApi.set('items', shortcuts); } async update(shortcut: Shortcut) { - const shortcuts = this.snapshot().filter(s => s.id !== shortcut.id); + const shortcuts = this.get().filter(s => s.id !== shortcut.id); shortcuts.push(shortcut); await this.storageApi.set('items', shortcuts); diff --git a/plugins/shortcuts/src/api/ShortcutApi.ts b/plugins/shortcuts/src/api/ShortcutApi.ts index 542e77bac8..1f93baa66c 100644 --- a/plugins/shortcuts/src/api/ShortcutApi.ts +++ b/plugins/shortcuts/src/api/ShortcutApi.ts @@ -31,7 +31,7 @@ export interface ShortcutApi { /** * Returns an immediate snapshot of shortcuts, sorted by title */ - snapshot(): Shortcut[]; + get(): Shortcut[]; /** * Generates a unique id for the shortcut and then saves it. From 565071f25850391b0acb2d3d686f0b0996969c2c Mon Sep 17 00:00:00 2001 From: Alex Rybchenko Date: Fri, 22 Jul 2022 11:02:06 +0200 Subject: [PATCH 6/6] updated changeset Signed-off-by: Alex Rybchenko --- .changeset/violet-trees-play.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.changeset/violet-trees-play.md b/.changeset/violet-trees-play.md index c56635633c..00f1e156a8 100644 --- a/.changeset/violet-trees-play.md +++ b/.changeset/violet-trees-play.md @@ -2,4 +2,13 @@ '@backstage/plugin-shortcuts': minor --- -Fixed shortcuts initialization when using firestore +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()); +```