From 3533075dfd48a02ce386f69e35391baeb180eb83 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Fri, 20 May 2022 13:00:51 +0200 Subject: [PATCH 01/22] Replace SidebarContext with versioned provider and hook. Signed-off-by: Eric Peterson --- packages/app/src/components/Root/Root.tsx | 6 +- packages/core-components/api-report.md | 16 ++-- packages/core-components/package.json | 1 + .../src/layout/Sidebar/Bar.tsx | 6 +- .../src/layout/Sidebar/Intro.tsx | 4 +- .../src/layout/Sidebar/Items.tsx | 6 +- .../src/layout/Sidebar/MobileSidebar.tsx | 7 +- .../layout/Sidebar/SidebarContext.test.tsx | 68 ++++++++++++++++ .../src/layout/Sidebar/SidebarContext.tsx | 78 +++++++++++++++++++ .../src/layout/Sidebar/SidebarSubmenu.tsx | 4 +- .../src/layout/Sidebar/config.ts | 16 ---- .../src/layout/Sidebar/index.ts | 14 +--- .../packages/app/src/components/Root/Root.tsx | 6 +- .../src/components/Root/Root.tsx | 6 +- plugins/shortcuts/src/ShortcutItem.test.tsx | 6 +- plugins/shortcuts/src/Shortcuts.test.tsx | 6 +- 16 files changed, 190 insertions(+), 60 deletions(-) create mode 100644 packages/core-components/src/layout/Sidebar/SidebarContext.test.tsx create mode 100644 packages/core-components/src/layout/Sidebar/SidebarContext.tsx diff --git a/packages/app/src/components/Root/Root.tsx b/packages/app/src/components/Root/Root.tsx index ac62c81899..e90fe29b54 100644 --- a/packages/app/src/components/Root/Root.tsx +++ b/packages/app/src/components/Root/Root.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { useContext, PropsWithChildren } from 'react'; +import React, { PropsWithChildren } from 'react'; import { Link, makeStyles } from '@material-ui/core'; import HomeIcon from '@material-ui/icons/Home'; import ExtensionIcon from '@material-ui/icons/Extension'; @@ -39,13 +39,13 @@ import { Shortcuts } from '@backstage/plugin-shortcuts'; import { Sidebar, sidebarConfig, - SidebarContext, SidebarDivider, SidebarGroup, SidebarItem, SidebarPage, SidebarScrollWrapper, SidebarSpace, + useSidebar, } from '@backstage/core-components'; import { MyGroupsSidebarItem } from '@backstage/plugin-org'; import GroupIcon from '@material-ui/icons/People'; @@ -68,7 +68,7 @@ const useSidebarLogoStyles = makeStyles({ const SidebarLogo = () => { const classes = useSidebarLogoStyles(); - const { isOpen } = useContext(SidebarContext); + const { isOpen } = useSidebar(); return (
diff --git a/packages/core-components/api-report.md b/packages/core-components/api-report.md index 727a50926c..e7ccc8e72b 100644 --- a/packages/core-components/api-report.md +++ b/packages/core-components/api-report.md @@ -16,7 +16,6 @@ import { CardHeaderProps } from '@material-ui/core/CardHeader'; import { Column } from '@material-table/core'; import { ComponentClass } from 'react'; import { ComponentProps } from 'react'; -import { Context } from 'react'; import { default as CSS_2 } from 'csstype'; import { CSSProperties } from 'react'; import { ElementType } from 'react'; @@ -903,13 +902,15 @@ export const sidebarConfig: { mobileSidebarHeight: number; }; -// Warning: (ae-missing-release-tag) "SidebarContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public -export const SidebarContext: Context; +export const SidebarContextProvider: ({ + children, + value, +}: { + children: ReactNode; + value: SidebarContextType; +}) => JSX.Element; -// Warning: (ae-missing-release-tag) "SidebarContextType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type SidebarContextType = { isOpen: boolean; @@ -1447,6 +1448,9 @@ export class UserIdentity implements IdentityApi { signOut(): Promise; } +// @public +export const useSidebar: () => SidebarContextType; + // Warning: (ae-missing-release-tag) "useSupportConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/packages/core-components/package.json b/packages/core-components/package.json index a988d8213e..6c7424af6e 100644 --- a/packages/core-components/package.json +++ b/packages/core-components/package.json @@ -37,6 +37,7 @@ "@backstage/core-plugin-api": "^1.0.2", "@backstage/errors": "^1.0.0", "@backstage/theme": "^0.2.15", + "@backstage/version-bridge": "^1.0.1", "@material-table/core": "^3.1.0", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", diff --git a/packages/core-components/src/layout/Sidebar/Bar.tsx b/packages/core-components/src/layout/Sidebar/Bar.tsx index e78706bd62..2fdb2012dc 100644 --- a/packages/core-components/src/layout/Sidebar/Bar.tsx +++ b/packages/core-components/src/layout/Sidebar/Bar.tsx @@ -25,7 +25,6 @@ import { makeSidebarConfig, makeSidebarSubmenuConfig, SidebarConfig, - SidebarContext, SidebarConfigContext, SubmenuConfig, SidebarOptions, @@ -33,6 +32,7 @@ import { } from './config'; import { BackstageTheme } from '@backstage/theme'; import { SidebarPinStateContext, useContent } from './Page'; +import { SidebarContextProvider } from './SidebarContext'; import { MobileSidebar } from './MobileSidebar'; /** @public */ @@ -191,7 +191,7 @@ const DesktopSidebar = (props: DesktopSidebarProps) => { return (
- + ); }; diff --git a/packages/core-components/src/layout/Sidebar/Intro.tsx b/packages/core-components/src/layout/Sidebar/Intro.tsx index d1d8f4b2a2..51d453b22d 100644 --- a/packages/core-components/src/layout/Sidebar/Intro.tsx +++ b/packages/core-components/src/layout/Sidebar/Intro.tsx @@ -25,10 +25,10 @@ import { useLocalStorageValue } from '@react-hookz/web'; import { SidebarConfigContext, SidebarConfig, - SidebarContext, SIDEBAR_INTRO_LOCAL_STORAGE, } from './config'; import { SidebarDivider } from './Items'; +import { useSidebar } from './SidebarContext'; /** @public */ export type SidebarIntroClassKey = @@ -151,7 +151,7 @@ const recentlyViewedIntroText = 'And your recently viewed plugins will pop up here!'; export function SidebarIntro(_props: {}) { - const { isOpen } = useContext(SidebarContext); + const { isOpen } = useSidebar(); const defaultValue = { starredItemsDismissed: false, recentlyViewedItemsDismissed: false, diff --git a/packages/core-components/src/layout/Sidebar/Items.tsx b/packages/core-components/src/layout/Sidebar/Items.tsx index fde3c85c96..3d294a54a8 100644 --- a/packages/core-components/src/layout/Sidebar/Items.tsx +++ b/packages/core-components/src/layout/Sidebar/Items.tsx @@ -48,7 +48,6 @@ import { useResolvedPath, } from 'react-router-dom'; import { - SidebarContext, SidebarConfigContext, SidebarItemWithSubmenuContext, SidebarConfig, @@ -62,6 +61,7 @@ import DoubleArrowLeft from './icons/DoubleArrowLeft'; import DoubleArrowRight from './icons/DoubleArrowRight'; import { isLocationMatch } from './utils'; import { Location } from 'history'; +import { useSidebar } from './SidebarContext'; /** @public */ export type SidebarItemClassKey = @@ -369,7 +369,7 @@ const SidebarItemBase = forwardRef((props, ref) => { // XXX (@koroeskohr): unsure this is optimal. But I just really didn't want to have the item component // depend on the current location, and at least have it being optionally forced to selected. // Still waiting on a Q answered to fine tune the implementation - const { isOpen } = useContext(SidebarContext); + const { isOpen } = useSidebar(); const divStyle = !isOpen && hasSubmenu ? { display: 'flex', marginLeft: '24px' } : {}; @@ -671,7 +671,7 @@ export const SidebarScrollWrapper = styled('div')(({ theme }) => { export const SidebarExpandButton = () => { const { sidebarConfig } = useContext(SidebarConfigContext); const classes = useMemoStyles(sidebarConfig); - const { isOpen, setOpen } = useContext(SidebarContext); + const { isOpen, setOpen } = useSidebar(); const isSmallScreen = useMediaQuery( theme => theme.breakpoints.down('md'), { noSsr: true }, diff --git a/packages/core-components/src/layout/Sidebar/MobileSidebar.tsx b/packages/core-components/src/layout/Sidebar/MobileSidebar.tsx index 111bf38314..46f38d9a2c 100644 --- a/packages/core-components/src/layout/Sidebar/MobileSidebar.tsx +++ b/packages/core-components/src/layout/Sidebar/MobileSidebar.tsx @@ -27,8 +27,9 @@ import MenuIcon from '@material-ui/icons/Menu'; import { orderBy } from 'lodash'; import React, { createContext, useEffect, useState, useContext } from 'react'; import { useLocation } from 'react-router'; +import { SidebarContextProvider } from './SidebarContext'; import { SidebarGroup } from './SidebarGroup'; -import { SidebarConfigContext, SidebarContext, SidebarConfig } from './config'; +import { SidebarConfigContext, SidebarConfig } from './config'; /** * Type of `MobileSidebarContext` @@ -207,7 +208,7 @@ export const MobileSidebar = (props: MobileSidebarProps) => { !sidebarGroups[selectedMenuItemIndex].props.to; return ( - {} }}> + {} }}> @@ -231,6 +232,6 @@ export const MobileSidebar = (props: MobileSidebarProps) => { {sidebarGroups} - + ); }; diff --git a/packages/core-components/src/layout/Sidebar/SidebarContext.test.tsx b/packages/core-components/src/layout/Sidebar/SidebarContext.test.tsx new file mode 100644 index 0000000000..d14e035433 --- /dev/null +++ b/packages/core-components/src/layout/Sidebar/SidebarContext.test.tsx @@ -0,0 +1,68 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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, { ReactNode } from 'react'; +import { renderInTestApp } from '@backstage/test-utils'; +import { screen, waitFor } from '@testing-library/react'; +import { renderHook, act } from '@testing-library/react-hooks'; +import { SidebarContextProvider, useSidebar } from './SidebarContext'; + +describe('SidebarContext', () => { + describe('SidebarContextProvider', () => { + it('should render children', async () => { + await renderInTestApp( + {} }}> + Child + , + ); + expect(await screen.findByText('Child')).toBeInTheDocument(); + }); + }); + + describe('useSidebar', () => { + it('does not need to be invoked within provider', () => { + const { result } = renderHook(() => useSidebar()); + expect(result.current.isOpen).toBe(false); + expect(typeof result.current.setOpen).toBe('function'); + }); + + it('should read and update state', async () => { + let actualValue = true; + const wrapper = ({ children }: { children: ReactNode }) => ( + { + actualValue = value; + }, + }} + > + {children} + + ); + const { result } = renderHook(() => useSidebar(), { wrapper }); + + expect(result.current.isOpen).toBe(true); + + act(() => { + result.current.setOpen(false); + }); + + waitFor(() => { + expect(result.current.isOpen).toBe(false); + }); + }); + }); +}); diff --git a/packages/core-components/src/layout/Sidebar/SidebarContext.tsx b/packages/core-components/src/layout/Sidebar/SidebarContext.tsx new file mode 100644 index 0000000000..c91be85928 --- /dev/null +++ b/packages/core-components/src/layout/Sidebar/SidebarContext.tsx @@ -0,0 +1,78 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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, { ReactNode, useContext } from 'react'; +import { + createVersionedContext, + createVersionedValueMap, +} from '@backstage/version-bridge'; + +/** + * Types for the `SidebarContext` + * + * @public + */ +export type SidebarContextType = { + isOpen: boolean; + setOpen: (open: boolean) => void; +}; + +const VersionedSidebarContext = createVersionedContext<{ + 1: SidebarContextType; +}>('sidebar-context'); + +/** + * Provides context for reading and updating sidebar state. + * + * @public + */ +export const SidebarContextProvider = ({ + children, + value, +}: { + children: ReactNode; + value: SidebarContextType; +}) => ( + + {children} + +); + +/** + * Hook to read and update sidebar state. + * + * @public + */ +export const useSidebar = (): SidebarContextType => { + const versionedSidebarContext = useContext(VersionedSidebarContext); + + // Invoked from outside a SidbarContextProvider, return a default value. + if (versionedSidebarContext === undefined) { + return { + isOpen: false, + setOpen: () => {}, + }; + } + + const sidebarContext = versionedSidebarContext.atVersion(1); + if (sidebarContext === undefined) { + throw new Error('No context found for version 1.'); + } + + return sidebarContext; +}; diff --git a/packages/core-components/src/layout/Sidebar/SidebarSubmenu.tsx b/packages/core-components/src/layout/Sidebar/SidebarSubmenu.tsx index d1b79263eb..d8a87c98d9 100644 --- a/packages/core-components/src/layout/Sidebar/SidebarSubmenu.tsx +++ b/packages/core-components/src/layout/Sidebar/SidebarSubmenu.tsx @@ -19,10 +19,10 @@ import classnames from 'classnames'; import React, { ReactNode, useContext, useEffect, useState } from 'react'; import { SidebarItemWithSubmenuContext, - SidebarContext, SidebarConfigContext, SubmenuConfig, } from './config'; +import { useSidebar } from './SidebarContext'; import { BackstageTheme } from '@backstage/theme'; const useStyles = makeStyles< @@ -105,7 +105,7 @@ export type SidebarSubmenuProps = { * @public */ export const SidebarSubmenu = (props: SidebarSubmenuProps) => { - const { isOpen } = useContext(SidebarContext); + const { isOpen } = useSidebar(); const { sidebarConfig, submenuConfig } = useContext(SidebarConfigContext); const left = isOpen ? sidebarConfig.drawerWidthOpen diff --git a/packages/core-components/src/layout/Sidebar/config.ts b/packages/core-components/src/layout/Sidebar/config.ts index 08d469d5a3..bb0ff207fb 100644 --- a/packages/core-components/src/layout/Sidebar/config.ts +++ b/packages/core-components/src/layout/Sidebar/config.ts @@ -101,22 +101,6 @@ export const makeSidebarSubmenuConfig = ( export const SIDEBAR_INTRO_LOCAL_STORAGE = '@backstage/core/sidebar-intro-dismissed'; -/** - * Types for the `SidebarContext` - */ -export type SidebarContextType = { - isOpen: boolean; - setOpen: (open: boolean) => void; -}; - -/** - * Context whether the `Sidebar` is open - */ -export const SidebarContext = createContext({ - isOpen: false, - setOpen: () => {}, -}); - export type SidebarConfigContextType = { sidebarConfig: SidebarConfig; submenuConfig: SubmenuConfig; diff --git a/packages/core-components/src/layout/Sidebar/index.ts b/packages/core-components/src/layout/Sidebar/index.ts index ecc573fbda..a852b35bf8 100644 --- a/packages/core-components/src/layout/Sidebar/index.ts +++ b/packages/core-components/src/layout/Sidebar/index.ts @@ -54,13 +54,7 @@ export type { } from './Items'; export { IntroCard, SidebarIntro } from './Intro'; export type { SidebarIntroClassKey } from './Intro'; -export { - SIDEBAR_INTRO_LOCAL_STORAGE, - SidebarContext, - sidebarConfig, -} from './config'; -export type { - SidebarContextType, - SidebarOptions, - SubmenuOptions, -} from './config'; +export { SIDEBAR_INTRO_LOCAL_STORAGE, sidebarConfig } from './config'; +export type { SidebarOptions, SubmenuOptions } from './config'; +export { SidebarContextProvider, useSidebar } from './SidebarContext'; +export type { SidebarContextType } from './SidebarContext'; diff --git a/packages/create-app/templates/default-app/packages/app/src/components/Root/Root.tsx b/packages/create-app/templates/default-app/packages/app/src/components/Root/Root.tsx index d10eccf03a..05e11458ce 100644 --- a/packages/create-app/templates/default-app/packages/app/src/components/Root/Root.tsx +++ b/packages/create-app/templates/default-app/packages/app/src/components/Root/Root.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { useContext, PropsWithChildren } from 'react'; +import React, { PropsWithChildren } from 'react'; import { Link, makeStyles } from '@material-ui/core'; import HomeIcon from '@material-ui/icons/Home'; import ExtensionIcon from '@material-ui/icons/Extension'; @@ -32,13 +32,13 @@ import { SidebarSearchModal } from '@backstage/plugin-search'; import { Sidebar, sidebarConfig, - SidebarContext, SidebarDivider, SidebarGroup, SidebarItem, SidebarPage, SidebarScrollWrapper, SidebarSpace, + useSidebar, } from '@backstage/core-components'; import MenuIcon from '@material-ui/icons/Menu'; import SearchIcon from '@material-ui/icons/Search'; @@ -60,7 +60,7 @@ const useSidebarLogoStyles = makeStyles({ const SidebarLogo = () => { const classes = useSidebarLogoStyles(); - const { isOpen } = useContext(SidebarContext); + const { isOpen } = useSidebar(); return (
diff --git a/packages/techdocs-cli-embedded-app/src/components/Root/Root.tsx b/packages/techdocs-cli-embedded-app/src/components/Root/Root.tsx index 249394fdc4..6613e7c0ea 100644 --- a/packages/techdocs-cli-embedded-app/src/components/Root/Root.tsx +++ b/packages/techdocs-cli-embedded-app/src/components/Root/Root.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { PropsWithChildren, useContext } from 'react'; +import React, { PropsWithChildren } from 'react'; import { Link, makeStyles } from '@material-ui/core'; import LibraryBooks from '@material-ui/icons/LibraryBooks'; @@ -27,7 +27,7 @@ import { SidebarPage, sidebarConfig, SidebarDivider, - SidebarContext, + useSidebar, } from '@backstage/core-components'; import { NavLink } from 'react-router-dom'; @@ -48,7 +48,7 @@ const useSidebarLogoStyles = makeStyles({ const SidebarLogo = () => { const classes = useSidebarLogoStyles(); - const { isOpen } = useContext(SidebarContext); + const { isOpen } = useSidebar(); return (
diff --git a/plugins/shortcuts/src/ShortcutItem.test.tsx b/plugins/shortcuts/src/ShortcutItem.test.tsx index 90b8ff06e5..d1a1018513 100644 --- a/plugins/shortcuts/src/ShortcutItem.test.tsx +++ b/plugins/shortcuts/src/ShortcutItem.test.tsx @@ -20,7 +20,7 @@ import { ShortcutItem } from './ShortcutItem'; import { Shortcut } from './types'; import { LocalStoredShortcuts } from './api'; import { MockStorageApi, renderInTestApp } from '@backstage/test-utils'; -import { SidebarContext } from '@backstage/core-components'; +import { SidebarContextProvider } from '@backstage/core-components'; describe('ShortcutItem', () => { const shortcut: Shortcut = { @@ -32,9 +32,9 @@ describe('ShortcutItem', () => { it('displays the shortcut', async () => { await renderInTestApp( - {} }}> + {} }}> - , + , ); expect(screen.getByText('ST')).toBeInTheDocument(); expect(screen.getByText('some title')).toBeInTheDocument(); diff --git a/plugins/shortcuts/src/Shortcuts.test.tsx b/plugins/shortcuts/src/Shortcuts.test.tsx index 8cee9a6622..813dbb8def 100644 --- a/plugins/shortcuts/src/Shortcuts.test.tsx +++ b/plugins/shortcuts/src/Shortcuts.test.tsx @@ -24,12 +24,12 @@ import { screen, waitFor } from '@testing-library/react'; import { Shortcuts } from './Shortcuts'; import { LocalStoredShortcuts, shortcutsApiRef } from './api'; -import { SidebarContext } from '@backstage/core-components'; +import { SidebarContextProvider } from '@backstage/core-components'; describe('Shortcuts', () => { it('displays an add button', async () => { await renderInTestApp( - {} }}> + {} }}> { > - , + , ); await waitFor(() => !screen.queryByTestId('progress')); expect(screen.getByText('Add Shortcuts')).toBeInTheDocument(); From da72da5daee9ac5cd54e43bbc9c2db13b51b6f53 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Fri, 20 May 2022 15:23:39 +0200 Subject: [PATCH 02/22] Replace SidebarPinStateContext with versioned provider and hook. Signed-off-by: Eric Peterson --- .../app/src/components/search/SearchPage.tsx | 6 +- packages/core-components/api-report.md | 11 ++- .../core-components/src/layout/Page/Page.tsx | 6 +- .../src/layout/Sidebar/Bar.test.tsx | 6 +- .../src/layout/Sidebar/Bar.tsx | 9 +-- .../src/layout/Sidebar/Page.tsx | 29 +------ .../src/layout/Sidebar/SidebarGroup.tsx | 4 +- .../Sidebar/SidebarPinStateContext.test.tsx | 79 +++++++++++++++++++ .../layout/Sidebar/SidebarPinStateContext.tsx | 79 +++++++++++++++++++ .../src/layout/Sidebar/index.ts | 17 ++-- .../reader/transformers/styles/transformer.ts | 6 +- .../General/UserSettingsAppearanceCard.tsx | 6 +- .../General/UserSettingsPinToggle.test.tsx | 6 +- .../General/UserSettingsPinToggle.tsx | 8 +- .../src/components/SettingsPage.tsx | 6 +- 15 files changed, 208 insertions(+), 70 deletions(-) create mode 100644 packages/core-components/src/layout/Sidebar/SidebarPinStateContext.test.tsx create mode 100644 packages/core-components/src/layout/Sidebar/SidebarPinStateContext.tsx diff --git a/packages/app/src/components/search/SearchPage.tsx b/packages/app/src/components/search/SearchPage.tsx index d1b56bbaf0..d23ad957d7 100644 --- a/packages/app/src/components/search/SearchPage.tsx +++ b/packages/app/src/components/search/SearchPage.tsx @@ -21,7 +21,7 @@ import { Header, Lifecycle, Page, - SidebarPinStateContext, + useSidebarPinState, } from '@backstage/core-components'; import { useApi } from '@backstage/core-plugin-api'; import { CatalogSearchResultListItem } from '@backstage/plugin-catalog'; @@ -40,7 +40,7 @@ import { import { useSearch } from '@backstage/plugin-search-react'; import { TechDocsSearchResultListItem } from '@backstage/plugin-techdocs'; import { Grid, List, makeStyles, Paper, Theme } from '@material-ui/core'; -import React, { useContext } from 'react'; +import React from 'react'; const useStyles = makeStyles((theme: Theme) => ({ bar: { @@ -59,7 +59,7 @@ const useStyles = makeStyles((theme: Theme) => ({ const SearchPage = () => { const classes = useStyles(); - const { isMobile } = useContext(SidebarPinStateContext); + const { isMobile } = useSidebarPinState(); const { types } = useSearch(); const catalogApi = useApi(catalogApiRef); diff --git a/packages/core-components/api-report.md b/packages/core-components/api-report.md index e7ccc8e72b..d162e7a714 100644 --- a/packages/core-components/api-report.md +++ b/packages/core-components/api-report.md @@ -1006,7 +1006,13 @@ export type SidebarPageProps = { }; // @public -export const SidebarPinStateContext: React_2.Context; +export const SidebarPinStateContextProvider: ({ + children, + value, +}: { + children: ReactNode; + value: SidebarPinStateContextType; +}) => JSX.Element; // @public export type SidebarPinStateContextType = { @@ -1451,6 +1457,9 @@ export class UserIdentity implements IdentityApi { // @public export const useSidebar: () => SidebarContextType; +// @public +export const useSidebarPinState: () => SidebarPinStateContextType; + // Warning: (ae-missing-release-tag) "useSupportConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/packages/core-components/src/layout/Page/Page.tsx b/packages/core-components/src/layout/Page/Page.tsx index 9bfc16fea3..1abb8da93b 100644 --- a/packages/core-components/src/layout/Page/Page.tsx +++ b/packages/core-components/src/layout/Page/Page.tsx @@ -14,10 +14,10 @@ * limitations under the License. */ -import React, { useContext } from 'react'; +import React from 'react'; import { BackstageTheme } from '@backstage/theme'; import { makeStyles, ThemeProvider } from '@material-ui/core/styles'; -import { SidebarPinStateContext } from '../Sidebar/Page'; +import { useSidebarPinState } from '../Sidebar/SidebarPinStateContext'; export type PageClassKey = 'root'; @@ -43,7 +43,7 @@ type Props = { export function Page(props: Props) { const { themeId, children } = props; - const { isMobile } = useContext(SidebarPinStateContext); + const { isMobile } = useSidebarPinState(); const classes = useStyles({ isMobile }); return ( - , + , ); } diff --git a/packages/core-components/src/layout/Sidebar/Bar.tsx b/packages/core-components/src/layout/Sidebar/Bar.tsx index 2fdb2012dc..a315406b58 100644 --- a/packages/core-components/src/layout/Sidebar/Bar.tsx +++ b/packages/core-components/src/layout/Sidebar/Bar.tsx @@ -31,8 +31,9 @@ import { SubmenuOptions, } from './config'; import { BackstageTheme } from '@backstage/theme'; -import { SidebarPinStateContext, useContent } from './Page'; +import { useContent } from './Page'; import { SidebarContextProvider } from './SidebarContext'; +import { useSidebarPinState } from './SidebarPinStateContext'; import { MobileSidebar } from './MobileSidebar'; /** @public */ @@ -133,9 +134,7 @@ const DesktopSidebar = (props: DesktopSidebarProps) => { ); const [state, setState] = useState(State.Closed); const hoverTimerRef = useRef(); - const { isPinned, toggleSidebarPinState } = useContext( - SidebarPinStateContext, - ); + const { isPinned, toggleSidebarPinState } = useSidebarPinState(); const handleOpen = () => { if (isPinned || disableExpandOnHover) { @@ -226,7 +225,7 @@ export const Sidebar = (props: SidebarProps) => { props.submenuOptions ?? {}, ); const { children, disableExpandOnHover, openDelayMs, closeDelayMs } = props; - const { isMobile } = useContext(SidebarPinStateContext); + const { isMobile } = useSidebarPinState(); return isMobile ? ( {children} diff --git a/packages/core-components/src/layout/Sidebar/Page.tsx b/packages/core-components/src/layout/Sidebar/Page.tsx index 9bf8105f81..17cd5ddee1 100644 --- a/packages/core-components/src/layout/Sidebar/Page.tsx +++ b/packages/core-components/src/layout/Sidebar/Page.tsx @@ -29,6 +29,7 @@ import { SidebarConfigContext, SidebarConfig } from './config'; import { BackstageTheme } from '@backstage/theme'; import { LocalStorage } from './localStorage'; import useMediaQuery from '@material-ui/core/useMediaQuery'; +import { SidebarPinStateContextProvider } from './SidebarPinStateContext'; export type SidebarPageClassKey = 'root'; @@ -62,17 +63,6 @@ const useStyles = makeStyles< { name: 'BackstageSidebarPage' }, ); -/** - * Type of `SidebarPinStateContext` - * - * @public - */ -export type SidebarPinStateContextType = { - isPinned: boolean; - toggleSidebarPinState: () => any; - isMobile?: boolean; -}; - /** * Props for SidebarPage * @@ -82,19 +72,6 @@ export type SidebarPageProps = { children?: React.ReactNode; }; -/** - * Contains the state on how the `Sidebar` is rendered - * - * @public - */ -export const SidebarPinStateContext = createContext( - { - isPinned: true, - toggleSidebarPinState: () => {}, - isMobile: false, - }, -); - type PageContextType = { content: { contentRef?: React.MutableRefObject; @@ -137,7 +114,7 @@ export function SidebarPage(props: SidebarPageProps) { const classes = useStyles({ isPinned, sidebarConfig }); return ( -
{props.children}
-
+ ); } diff --git a/packages/core-components/src/layout/Sidebar/SidebarGroup.tsx b/packages/core-components/src/layout/Sidebar/SidebarGroup.tsx index 55b5e770b0..1158cc14df 100644 --- a/packages/core-components/src/layout/Sidebar/SidebarGroup.tsx +++ b/packages/core-components/src/layout/Sidebar/SidebarGroup.tsx @@ -22,7 +22,7 @@ import BottomNavigationAction, { import { makeStyles } from '@material-ui/core/styles'; import React, { useContext } from 'react'; import { useLocation } from 'react-router-dom'; -import { SidebarPinStateContext } from '.'; +import { useSidebarPinState } from '.'; import { Link } from '../../components'; import { SidebarConfigContext, SidebarConfig } from './config'; import { MobileSidebarContext } from './MobileSidebar'; @@ -122,7 +122,7 @@ const MobileSidebarGroup = (props: SidebarGroupProps) => { */ export const SidebarGroup = (props: SidebarGroupProps) => { const { children, to, label, icon, value } = props; - const { isMobile } = useContext(SidebarPinStateContext); + const { isMobile } = useSidebarPinState(); return isMobile ? ( diff --git a/packages/core-components/src/layout/Sidebar/SidebarPinStateContext.test.tsx b/packages/core-components/src/layout/Sidebar/SidebarPinStateContext.test.tsx new file mode 100644 index 0000000000..cc83a846e3 --- /dev/null +++ b/packages/core-components/src/layout/Sidebar/SidebarPinStateContext.test.tsx @@ -0,0 +1,79 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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, { ReactNode } from 'react'; +import { renderInTestApp } from '@backstage/test-utils'; +import { screen, waitFor } from '@testing-library/react'; +import { renderHook, act } from '@testing-library/react-hooks'; +import { + SidebarPinStateContextProvider, + useSidebarPinState, +} from './SidebarPinStateContext'; + +describe('SidebarContext', () => { + describe('SidebarContextProvider', () => { + it('should render children', async () => { + await renderInTestApp( + {}, + }} + > + Child + , + ); + expect(await screen.findByText('Child')).toBeInTheDocument(); + }); + }); + + describe('useSidebar', () => { + it('does not need to be invoked within provider', () => { + const { result } = renderHook(() => useSidebarPinState()); + expect(result.current.isPinned).toBe(true); + expect(result.current.isMobile).toBe(false); + expect(typeof result.current.toggleSidebarPinState).toBe('function'); + }); + + it('should read and update state', async () => { + let actualValue = true; + const wrapper = ({ children }: { children: ReactNode }) => ( + { + actualValue = !actualValue; + }, + }} + > + {children} + + ); + const { result } = renderHook(() => useSidebarPinState(), { wrapper }); + + expect(result.current.isPinned).toBe(true); + + act(() => { + result.current.toggleSidebarPinState(); + }); + + waitFor(() => { + expect(result.current.isPinned).toBe(false); + }); + }); + }); +}); diff --git a/packages/core-components/src/layout/Sidebar/SidebarPinStateContext.tsx b/packages/core-components/src/layout/Sidebar/SidebarPinStateContext.tsx new file mode 100644 index 0000000000..709973e064 --- /dev/null +++ b/packages/core-components/src/layout/Sidebar/SidebarPinStateContext.tsx @@ -0,0 +1,79 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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 { + createVersionedContext, + createVersionedValueMap, +} from '@backstage/version-bridge'; +import React, { ReactNode, useContext } from 'react'; + +/** + * Type of `SidebarPinStateContext` + * + * @public + */ +export type SidebarPinStateContextType = { + isPinned: boolean; + toggleSidebarPinState: () => any; + isMobile?: boolean; +}; + +const VersionedSidebarPinStateContext = createVersionedContext<{ + 1: SidebarPinStateContextType; +}>('sidebar-pin-state-context'); + +/** + * Provides state for how the `Sidebar` is rendered + * + * @public + */ +export const SidebarPinStateContextProvider = ({ + children, + value, +}: { + children: ReactNode; + value: SidebarPinStateContextType; +}) => ( + + {children} + +); + +/** + * Hook to read and update sidebar pin state. + * + * @public + */ +export const useSidebarPinState = (): SidebarPinStateContextType => { + const versionedSidebarContext = useContext(VersionedSidebarPinStateContext); + + // Invoked from outside a SidebarPinStateContextProvider: default value. + if (versionedSidebarContext === undefined) { + return { + isPinned: true, + toggleSidebarPinState: () => {}, + isMobile: false, + }; + } + + const sidebarContext = versionedSidebarContext.atVersion(1); + if (sidebarContext === undefined) { + throw new Error('No context found for version 1.'); + } + + return sidebarContext; +}; diff --git a/packages/core-components/src/layout/Sidebar/index.ts b/packages/core-components/src/layout/Sidebar/index.ts index a852b35bf8..e041ecc730 100644 --- a/packages/core-components/src/layout/Sidebar/index.ts +++ b/packages/core-components/src/layout/Sidebar/index.ts @@ -27,16 +27,8 @@ export type { SidebarSubmenuItemDropdownItem, } from './SidebarSubmenuItem'; export type { SidebarClassKey, SidebarProps } from './Bar'; -export { - SidebarPage, - SidebarPinStateContext as SidebarPinStateContext, - useContent, -} from './Page'; -export type { - SidebarPinStateContextType as SidebarPinStateContextType, - SidebarPageClassKey, - SidebarPageProps, -} from './Page'; +export { SidebarPage, useContent } from './Page'; +export type { SidebarPageClassKey, SidebarPageProps } from './Page'; export { SidebarDivider, SidebarItem, @@ -58,3 +50,8 @@ export { SIDEBAR_INTRO_LOCAL_STORAGE, sidebarConfig } from './config'; export type { SidebarOptions, SubmenuOptions } from './config'; export { SidebarContextProvider, useSidebar } from './SidebarContext'; export type { SidebarContextType } from './SidebarContext'; +export { + SidebarPinStateContextProvider, + useSidebarPinState, +} from './SidebarPinStateContext'; +export type { SidebarPinStateContextType } from './SidebarPinStateContext'; diff --git a/plugins/techdocs/src/reader/transformers/styles/transformer.ts b/plugins/techdocs/src/reader/transformers/styles/transformer.ts index 26ab4ceb26..04a6d476af 100644 --- a/plugins/techdocs/src/reader/transformers/styles/transformer.ts +++ b/plugins/techdocs/src/reader/transformers/styles/transformer.ts @@ -14,11 +14,11 @@ * limitations under the License. */ -import { useCallback, useContext, useMemo } from 'react'; +import { useCallback, useMemo } from 'react'; import { useTheme } from '@material-ui/core'; -import { SidebarPinStateContext } from '@backstage/core-components'; +import { useSidebarPinState } from '@backstage/core-components'; import { BackstageTheme } from '@backstage/theme'; import { Transformer } from '../transformer'; @@ -27,7 +27,7 @@ import { rules } from './rules'; /** * Sidebar pinned state to be used in computing style injections. */ -const useSidebar = () => useContext(SidebarPinStateContext); +const useSidebar = () => useSidebarPinState(); /** * Process all rules and concatenate their definitions into a single style. diff --git a/plugins/user-settings/src/components/General/UserSettingsAppearanceCard.tsx b/plugins/user-settings/src/components/General/UserSettingsAppearanceCard.tsx index 15ab93c6d0..cd55218db5 100644 --- a/plugins/user-settings/src/components/General/UserSettingsAppearanceCard.tsx +++ b/plugins/user-settings/src/components/General/UserSettingsAppearanceCard.tsx @@ -13,14 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { InfoCard, SidebarPinStateContext } from '@backstage/core-components'; +import { InfoCard, useSidebarPinState } from '@backstage/core-components'; import { List } from '@material-ui/core'; -import React, { useContext } from 'react'; +import React from 'react'; import { UserSettingsPinToggle } from './UserSettingsPinToggle'; import { UserSettingsThemeToggle } from './UserSettingsThemeToggle'; export const UserSettingsAppearanceCard = () => { - const { isMobile } = useContext(SidebarPinStateContext); + const { isMobile } = useSidebarPinState(); return ( diff --git a/plugins/user-settings/src/components/General/UserSettingsPinToggle.test.tsx b/plugins/user-settings/src/components/General/UserSettingsPinToggle.test.tsx index b63ac85b10..a41792768a 100644 --- a/plugins/user-settings/src/components/General/UserSettingsPinToggle.test.tsx +++ b/plugins/user-settings/src/components/General/UserSettingsPinToggle.test.tsx @@ -18,14 +18,14 @@ import { renderWithEffects, wrapInTestApp } from '@backstage/test-utils'; import { fireEvent } from '@testing-library/react'; import React from 'react'; import { UserSettingsPinToggle } from './UserSettingsPinToggle'; -import { SidebarPinStateContext } from '@backstage/core-components'; +import { SidebarPinStateContextProvider } from '@backstage/core-components'; describe('', () => { it('toggles the pin sidebar button', async () => { const mockToggleFn = jest.fn(); const rendered = await renderWithEffects( wrapInTestApp( - ', () => { }} > - , + , ), ); expect(rendered.getByText('Pin Sidebar')).toBeInTheDocument(); diff --git a/plugins/user-settings/src/components/General/UserSettingsPinToggle.tsx b/plugins/user-settings/src/components/General/UserSettingsPinToggle.tsx index 4d71df8113..d218787c05 100644 --- a/plugins/user-settings/src/components/General/UserSettingsPinToggle.tsx +++ b/plugins/user-settings/src/components/General/UserSettingsPinToggle.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { useContext } from 'react'; +import React from 'react'; import { ListItem, ListItemSecondaryAction, @@ -22,12 +22,10 @@ import { Switch, Tooltip, } from '@material-ui/core'; -import { SidebarPinStateContext } from '@backstage/core-components'; +import { useSidebarPinState } from '@backstage/core-components'; export const UserSettingsPinToggle = () => { - const { isPinned, toggleSidebarPinState } = useContext( - SidebarPinStateContext, - ); + const { isPinned, toggleSidebarPinState } = useSidebarPinState(); return ( diff --git a/plugins/user-settings/src/components/SettingsPage.tsx b/plugins/user-settings/src/components/SettingsPage.tsx index 8942235fda..dcaf62d435 100644 --- a/plugins/user-settings/src/components/SettingsPage.tsx +++ b/plugins/user-settings/src/components/SettingsPage.tsx @@ -17,10 +17,10 @@ import { Header, Page, - SidebarPinStateContext, TabbedLayout, + useSidebarPinState, } from '@backstage/core-components'; -import React, { useContext } from 'react'; +import React from 'react'; import { useOutlet } from 'react-router'; import { useElementFilter } from '@backstage/core-plugin-api'; import { UserSettingsAuthProviders } from './AuthProviders'; @@ -33,7 +33,7 @@ type Props = { }; export const SettingsPage = ({ providerSettings }: Props) => { - const { isMobile } = useContext(SidebarPinStateContext); + const { isMobile } = useSidebarPinState(); const outlet = useOutlet(); const tabs = useElementFilter(outlet, elements => From bff65e6958de04600bfcfe4bcd022cc6e0443b8f Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Fri, 20 May 2022 16:51:32 +0200 Subject: [PATCH 03/22] Changesets for affected packages. Signed-off-by: Eric Peterson --- .changeset/give-that-wolf-a-banana.md | 30 +++++++++++++++++++++++ .changeset/right-one-at-the-wrong-time.md | 7 ++++++ .changeset/up-in-space-man.md | 7 ++++++ 3 files changed, 44 insertions(+) create mode 100644 .changeset/give-that-wolf-a-banana.md create mode 100644 .changeset/right-one-at-the-wrong-time.md create mode 100644 .changeset/up-in-space-man.md diff --git a/.changeset/give-that-wolf-a-banana.md b/.changeset/give-that-wolf-a-banana.md new file mode 100644 index 0000000000..685ef7400a --- /dev/null +++ b/.changeset/give-that-wolf-a-banana.md @@ -0,0 +1,30 @@ +--- +'@backstage/create-app': patch +--- + +Use of `SidebarContext` has been deprecated and will be removed in a future release. Instead, `useSidebar()` should be used to consume the context and `` should be used to provide it. + +To prepare your app, update `packages/app/src/components/Root/Root.tsx` as follows: + +```diff +import { + Sidebar, + sidebarConfig, +- SidebarContext + SidebarDivider, + // ... + SidebarSpace, ++ useSidebar, +} from '@backstage/core-components'; + +// ... + + +const SidebarLogo = () => { + const classes = useSidebarLogoStyles(); +- const { isOpen } = useContext(SidebarContext); ++ const { isOpen } = useSidebar(); + + // ... +}; +``` diff --git a/.changeset/right-one-at-the-wrong-time.md b/.changeset/right-one-at-the-wrong-time.md new file mode 100644 index 0000000000..c7fee94a91 --- /dev/null +++ b/.changeset/right-one-at-the-wrong-time.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-techdocs': patch +'@backstage/plugin-user-settings': patch +'@techdocs/cli': patch +--- + +Updated sidebar-related logic to use `` + `useSidebarPinState()` and/or `` + `useSidebar()` from `@backstage/core-components`. diff --git a/.changeset/up-in-space-man.md b/.changeset/up-in-space-man.md new file mode 100644 index 0000000000..f3aadfece8 --- /dev/null +++ b/.changeset/up-in-space-man.md @@ -0,0 +1,7 @@ +--- +'@backstage/core-components': patch +--- + +The `SidebarPinStateContext` and `SidebarContext` have been deprecated and will be removed in a future release. Instead, use `` + `useSidebarPinState()` and/or `` + `useSidebar()`. + +This was done to ensure that sidebar state can be shared successfully across components exported by different packages, regardless of what version of this package is resolved and installed for each individual package. From a907d620fa771fc4a4c8f71e4adabdb2839a3446 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Fri, 20 May 2022 17:28:14 +0200 Subject: [PATCH 04/22] Provide a deprecation path for affected sidebar contexts. Signed-off-by: Eric Peterson --- packages/core-components/api-report.md | 6 +++ .../layout/Sidebar/SidebarContext.test.tsx | 41 +++++++++++++---- .../src/layout/Sidebar/SidebarContext.tsx | 34 +++++++++----- .../Sidebar/SidebarPinStateContext.test.tsx | 45 ++++++++++++++----- .../layout/Sidebar/SidebarPinStateContext.tsx | 35 ++++++++++----- .../src/layout/Sidebar/index.ts | 7 ++- 6 files changed, 128 insertions(+), 40 deletions(-) diff --git a/packages/core-components/api-report.md b/packages/core-components/api-report.md index d162e7a714..33db173fcf 100644 --- a/packages/core-components/api-report.md +++ b/packages/core-components/api-report.md @@ -902,6 +902,9 @@ export const sidebarConfig: { mobileSidebarHeight: number; }; +// @public @deprecated +export const SidebarContext: React_2.Context; + // @public export const SidebarContextProvider: ({ children, @@ -1005,6 +1008,9 @@ export type SidebarPageProps = { children?: React_2.ReactNode; }; +// @public @deprecated +export const SidebarPinStateContext: React_2.Context; + // @public export const SidebarPinStateContextProvider: ({ children, diff --git a/packages/core-components/src/layout/Sidebar/SidebarContext.test.tsx b/packages/core-components/src/layout/Sidebar/SidebarContext.test.tsx index d14e035433..43a18851d7 100644 --- a/packages/core-components/src/layout/Sidebar/SidebarContext.test.tsx +++ b/packages/core-components/src/layout/Sidebar/SidebarContext.test.tsx @@ -13,21 +13,45 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { ReactNode } from 'react'; -import { renderInTestApp } from '@backstage/test-utils'; -import { screen, waitFor } from '@testing-library/react'; +import React, { ReactNode, useContext } from 'react'; +import { renderWithEffects } from '@backstage/test-utils'; +import { waitFor } from '@testing-library/react'; import { renderHook, act } from '@testing-library/react-hooks'; -import { SidebarContextProvider, useSidebar } from './SidebarContext'; +import { + LegacySidebarContext, + SidebarContextProvider, + useSidebar, +} from './SidebarContext'; describe('SidebarContext', () => { describe('SidebarContextProvider', () => { it('should render children', async () => { - await renderInTestApp( + const { findByText } = await renderWithEffects( {} }}> Child , ); - expect(await screen.findByText('Child')).toBeInTheDocument(); + expect(await findByText('Child')).toBeInTheDocument(); + }); + + it('should provide the legacy context as well, for now', async () => { + const LegacyContextSpy = () => { + const { isOpen } = useContext(LegacySidebarContext); + return <>{String(isOpen)}; + }; + + const { findByText } = await renderWithEffects( + {}, + }} + > + + , + ); + + expect(await findByText('true')).toBeInTheDocument(); }); }); @@ -52,15 +76,16 @@ describe('SidebarContext', () => { {children} ); - const { result } = renderHook(() => useSidebar(), { wrapper }); + const { result, rerender } = renderHook(() => useSidebar(), { wrapper }); expect(result.current.isOpen).toBe(true); act(() => { result.current.setOpen(false); + rerender(); }); - waitFor(() => { + await waitFor(() => { expect(result.current.isOpen).toBe(false); }); }); diff --git a/packages/core-components/src/layout/Sidebar/SidebarContext.tsx b/packages/core-components/src/layout/Sidebar/SidebarContext.tsx index c91be85928..4e47b6040a 100644 --- a/packages/core-components/src/layout/Sidebar/SidebarContext.tsx +++ b/packages/core-components/src/layout/Sidebar/SidebarContext.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { ReactNode, useContext } from 'react'; +import React, { createContext, ReactNode, useContext } from 'react'; import { createVersionedContext, createVersionedValueMap, @@ -30,6 +30,21 @@ export type SidebarContextType = { setOpen: (open: boolean) => void; }; +const defaultSidebarContext = { + isOpen: false, + setOpen: () => {}, +}; + +/** + * Context whether the `Sidebar` is open + * + * @public @deprecated + * Use `` + `useSidebar()` instead. + */ +export const LegacySidebarContext = createContext( + defaultSidebarContext, +); + const VersionedSidebarContext = createVersionedContext<{ 1: SidebarContextType; }>('sidebar-context'); @@ -46,11 +61,13 @@ export const SidebarContextProvider = ({ children: ReactNode; value: SidebarContextType; }) => ( - - {children} - + + + {children} + + ); /** @@ -63,10 +80,7 @@ export const useSidebar = (): SidebarContextType => { // Invoked from outside a SidbarContextProvider, return a default value. if (versionedSidebarContext === undefined) { - return { - isOpen: false, - setOpen: () => {}, - }; + return defaultSidebarContext; } const sidebarContext = versionedSidebarContext.atVersion(1); diff --git a/packages/core-components/src/layout/Sidebar/SidebarPinStateContext.test.tsx b/packages/core-components/src/layout/Sidebar/SidebarPinStateContext.test.tsx index cc83a846e3..d8bc054e4a 100644 --- a/packages/core-components/src/layout/Sidebar/SidebarPinStateContext.test.tsx +++ b/packages/core-components/src/layout/Sidebar/SidebarPinStateContext.test.tsx @@ -13,19 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { ReactNode } from 'react'; -import { renderInTestApp } from '@backstage/test-utils'; -import { screen, waitFor } from '@testing-library/react'; +import React, { ReactNode, useContext } from 'react'; +import { renderWithEffects } from '@backstage/test-utils'; +import { waitFor } from '@testing-library/react'; import { renderHook, act } from '@testing-library/react-hooks'; import { + LegacySidebarPinStateContext, SidebarPinStateContextProvider, useSidebarPinState, } from './SidebarPinStateContext'; -describe('SidebarContext', () => { - describe('SidebarContextProvider', () => { +describe('SidebarPinStateContext', () => { + describe('SidebarPinStateContextProvider', () => { it('should render children', async () => { - await renderInTestApp( + const { findByText } = await renderWithEffects( { Child , ); - expect(await screen.findByText('Child')).toBeInTheDocument(); + expect(await findByText('Child')).toBeInTheDocument(); + }); + + it('should provide the legacy context as well, for now', async () => { + const LegacyContextSpy = () => { + const { isMobile } = useContext(LegacySidebarPinStateContext); + return <>{String(isMobile)}; + }; + + const { findByText } = await renderWithEffects( + {}, + }} + > + + , + ); + + expect(await findByText('true')).toBeInTheDocument(); }); }); - describe('useSidebar', () => { + describe('useSidebarPinState', () => { it('does not need to be invoked within provider', () => { const { result } = renderHook(() => useSidebarPinState()); expect(result.current.isPinned).toBe(true); @@ -63,15 +85,18 @@ describe('SidebarContext', () => { {children} ); - const { result } = renderHook(() => useSidebarPinState(), { wrapper }); + const { result, rerender } = renderHook(() => useSidebarPinState(), { + wrapper, + }); expect(result.current.isPinned).toBe(true); act(() => { result.current.toggleSidebarPinState(); + rerender(); }); - waitFor(() => { + await waitFor(() => { expect(result.current.isPinned).toBe(false); }); }); diff --git a/packages/core-components/src/layout/Sidebar/SidebarPinStateContext.tsx b/packages/core-components/src/layout/Sidebar/SidebarPinStateContext.tsx index 709973e064..ec38884890 100644 --- a/packages/core-components/src/layout/Sidebar/SidebarPinStateContext.tsx +++ b/packages/core-components/src/layout/Sidebar/SidebarPinStateContext.tsx @@ -17,7 +17,7 @@ import { createVersionedContext, createVersionedValueMap, } from '@backstage/version-bridge'; -import React, { ReactNode, useContext } from 'react'; +import React, { createContext, ReactNode, useContext } from 'react'; /** * Type of `SidebarPinStateContext` @@ -30,6 +30,21 @@ export type SidebarPinStateContextType = { isMobile?: boolean; }; +const defaultSidebarPinStateContext = { + isPinned: true, + toggleSidebarPinState: () => {}, + isMobile: false, +}; + +/** + * Contains the state on how the `Sidebar` is rendered + * + * @public @deprecated + * Use `` + `useSidebarPinState()` instead. + */ +export const LegacySidebarPinStateContext = + createContext(defaultSidebarPinStateContext); + const VersionedSidebarPinStateContext = createVersionedContext<{ 1: SidebarPinStateContextType; }>('sidebar-pin-state-context'); @@ -46,11 +61,13 @@ export const SidebarPinStateContextProvider = ({ children: ReactNode; value: SidebarPinStateContextType; }) => ( - - {children} - + + + {children} + + ); /** @@ -63,11 +80,7 @@ export const useSidebarPinState = (): SidebarPinStateContextType => { // Invoked from outside a SidebarPinStateContextProvider: default value. if (versionedSidebarContext === undefined) { - return { - isPinned: true, - toggleSidebarPinState: () => {}, - isMobile: false, - }; + return defaultSidebarPinStateContext; } const sidebarContext = versionedSidebarContext.atVersion(1); diff --git a/packages/core-components/src/layout/Sidebar/index.ts b/packages/core-components/src/layout/Sidebar/index.ts index e041ecc730..06681ded52 100644 --- a/packages/core-components/src/layout/Sidebar/index.ts +++ b/packages/core-components/src/layout/Sidebar/index.ts @@ -48,9 +48,14 @@ export { IntroCard, SidebarIntro } from './Intro'; export type { SidebarIntroClassKey } from './Intro'; export { SIDEBAR_INTRO_LOCAL_STORAGE, sidebarConfig } from './config'; export type { SidebarOptions, SubmenuOptions } from './config'; -export { SidebarContextProvider, useSidebar } from './SidebarContext'; +export { + LegacySidebarContext as SidebarContext, + SidebarContextProvider, + useSidebar, +} from './SidebarContext'; export type { SidebarContextType } from './SidebarContext'; export { + LegacySidebarPinStateContext as SidebarPinStateContext, SidebarPinStateContextProvider, useSidebarPinState, } from './SidebarPinStateContext'; From 37c8f8444c46b5ca442bd09f7f8685d4932244a3 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Fri, 20 May 2022 17:51:11 +0200 Subject: [PATCH 05/22] Better naming of providers, hooks, and types. Signed-off-by: Eric Peterson --- .changeset/give-that-wolf-a-banana.md | 7 ++- .changeset/right-one-at-the-wrong-time.md | 2 +- .changeset/up-in-space-man.md | 2 +- packages/app/src/components/Root/Root.tsx | 4 +- packages/core-components/api-report.md | 53 ++++++++++++------- .../src/layout/Sidebar/Bar.test.tsx | 6 +-- .../src/layout/Sidebar/Bar.tsx | 6 +-- .../src/layout/Sidebar/Intro.tsx | 4 +- .../src/layout/Sidebar/Items.tsx | 6 +-- .../src/layout/Sidebar/MobileSidebar.tsx | 6 +-- .../src/layout/Sidebar/Page.tsx | 6 +-- ...t.tsx => SidebarOpenStateContext.test.tsx} | 30 ++++++----- ...ontext.tsx => SidebarOpenStateContext.tsx} | 27 +++++++--- .../Sidebar/SidebarPinStateContext.test.tsx | 16 +++--- .../layout/Sidebar/SidebarPinStateContext.tsx | 22 ++++++-- .../src/layout/Sidebar/SidebarSubmenu.tsx | 4 +- .../src/layout/Sidebar/index.ts | 18 ++++--- .../packages/app/src/components/Root/Root.tsx | 4 +- .../src/components/Root/Root.tsx | 4 +- plugins/shortcuts/src/ShortcutItem.test.tsx | 6 +-- plugins/shortcuts/src/Shortcuts.test.tsx | 6 +-- .../General/UserSettingsPinToggle.test.tsx | 6 +-- 22 files changed, 144 insertions(+), 101 deletions(-) rename packages/core-components/src/layout/Sidebar/{SidebarContext.test.tsx => SidebarOpenStateContext.test.tsx} (78%) rename packages/core-components/src/layout/Sidebar/{SidebarContext.tsx => SidebarOpenStateContext.tsx} (79%) diff --git a/.changeset/give-that-wolf-a-banana.md b/.changeset/give-that-wolf-a-banana.md index 685ef7400a..a9e99782ad 100644 --- a/.changeset/give-that-wolf-a-banana.md +++ b/.changeset/give-that-wolf-a-banana.md @@ -2,7 +2,7 @@ '@backstage/create-app': patch --- -Use of `SidebarContext` has been deprecated and will be removed in a future release. Instead, `useSidebar()` should be used to consume the context and `` should be used to provide it. +Use of `SidebarContext` has been deprecated and will be removed in a future release. Instead, `useSidebarOpenState()` should be used to consume the context and `` should be used to provide it. To prepare your app, update `packages/app/src/components/Root/Root.tsx` as follows: @@ -14,16 +14,15 @@ import { SidebarDivider, // ... SidebarSpace, -+ useSidebar, ++ useSidebarOpenState, } from '@backstage/core-components'; // ... - const SidebarLogo = () => { const classes = useSidebarLogoStyles(); - const { isOpen } = useContext(SidebarContext); -+ const { isOpen } = useSidebar(); ++ const { isOpen } = useSidebarOpenState(); // ... }; diff --git a/.changeset/right-one-at-the-wrong-time.md b/.changeset/right-one-at-the-wrong-time.md index c7fee94a91..5ef8e42555 100644 --- a/.changeset/right-one-at-the-wrong-time.md +++ b/.changeset/right-one-at-the-wrong-time.md @@ -4,4 +4,4 @@ '@techdocs/cli': patch --- -Updated sidebar-related logic to use `` + `useSidebarPinState()` and/or `` + `useSidebar()` from `@backstage/core-components`. +Updated sidebar-related logic to use `` + `useSidebarPinState()` and/or `` + `useSidebarOpenState()` from `@backstage/core-components`. diff --git a/.changeset/up-in-space-man.md b/.changeset/up-in-space-man.md index f3aadfece8..3a4dc6c5d3 100644 --- a/.changeset/up-in-space-man.md +++ b/.changeset/up-in-space-man.md @@ -2,6 +2,6 @@ '@backstage/core-components': patch --- -The `SidebarPinStateContext` and `SidebarContext` have been deprecated and will be removed in a future release. Instead, use `` + `useSidebarPinState()` and/or `` + `useSidebar()`. +The `SidebarPinStateContext` and `SidebarContext` have been deprecated and will be removed in a future release. Instead, use `` + `useSidebarPinState()` and/or `` + `useSidebarOpenState()`. This was done to ensure that sidebar state can be shared successfully across components exported by different packages, regardless of what version of this package is resolved and installed for each individual package. diff --git a/packages/app/src/components/Root/Root.tsx b/packages/app/src/components/Root/Root.tsx index e90fe29b54..30da01841c 100644 --- a/packages/app/src/components/Root/Root.tsx +++ b/packages/app/src/components/Root/Root.tsx @@ -45,7 +45,7 @@ import { SidebarPage, SidebarScrollWrapper, SidebarSpace, - useSidebar, + useSidebarOpenState, } from '@backstage/core-components'; import { MyGroupsSidebarItem } from '@backstage/plugin-org'; import GroupIcon from '@material-ui/icons/People'; @@ -68,7 +68,7 @@ const useSidebarLogoStyles = makeStyles({ const SidebarLogo = () => { const classes = useSidebarLogoStyles(); - const { isOpen } = useSidebar(); + const { isOpen } = useSidebarOpenState(); return (
diff --git a/packages/core-components/api-report.md b/packages/core-components/api-report.md index 33db173fcf..911ef36867 100644 --- a/packages/core-components/api-report.md +++ b/packages/core-components/api-report.md @@ -905,16 +905,7 @@ export const sidebarConfig: { // @public @deprecated export const SidebarContext: React_2.Context; -// @public -export const SidebarContextProvider: ({ - children, - value, -}: { - children: ReactNode; - value: SidebarContextType; -}) => JSX.Element; - -// @public +// @public @deprecated export type SidebarContextType = { isOpen: boolean; setOpen: (open: boolean) => void; @@ -987,6 +978,21 @@ export type SidebarItemClassKey = | 'arrows' | 'selected'; +// @public +export type SidebarOpenState = { + isOpen: boolean; + setOpen: (open: boolean) => void; +}; + +// @public +export const SidebarOpenStateProvider: ({ + children, + value, +}: { + children: ReactNode; + value: SidebarOpenState; +}) => JSX.Element; + // @public (undocumented) export type SidebarOptions = { drawerWidthClosed?: number; @@ -1008,11 +1014,25 @@ export type SidebarPageProps = { children?: React_2.ReactNode; }; +// @public +export type SidebarPinState = { + isPinned: boolean; + toggleSidebarPinState: () => any; + isMobile?: boolean; +}; + // @public @deprecated export const SidebarPinStateContext: React_2.Context; +// @public @deprecated +export type SidebarPinStateContextType = { + isPinned: boolean; + toggleSidebarPinState: () => any; + isMobile?: boolean; +}; + // @public -export const SidebarPinStateContextProvider: ({ +export const SidebarPinStateProvider: ({ children, value, }: { @@ -1020,13 +1040,6 @@ export const SidebarPinStateContextProvider: ({ value: SidebarPinStateContextType; }) => JSX.Element; -// @public -export type SidebarPinStateContextType = { - isPinned: boolean; - toggleSidebarPinState: () => any; - isMobile?: boolean; -}; - // @public (undocumented) export type SidebarProps = { openDelayMs?: number; @@ -1461,10 +1474,10 @@ export class UserIdentity implements IdentityApi { } // @public -export const useSidebar: () => SidebarContextType; +export const useSidebarOpenState: () => SidebarOpenState; // @public -export const useSidebarPinState: () => SidebarPinStateContextType; +export const useSidebarPinState: () => SidebarPinState; // Warning: (ae-missing-release-tag) "useSupportConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // diff --git a/packages/core-components/src/layout/Sidebar/Bar.test.tsx b/packages/core-components/src/layout/Sidebar/Bar.test.tsx index ec9c08a1eb..87e62f15e0 100644 --- a/packages/core-components/src/layout/Sidebar/Bar.test.tsx +++ b/packages/core-components/src/layout/Sidebar/Bar.test.tsx @@ -27,14 +27,14 @@ import { SidebarExpandButton, SidebarItem, SidebarSearchField, - SidebarPinStateContextProvider, + SidebarPinStateProvider, SidebarSubmenu, SidebarSubmenuItem, } from '.'; async function renderScalableSidebar() { await renderInTestApp( - - , + , ); } diff --git a/packages/core-components/src/layout/Sidebar/Bar.tsx b/packages/core-components/src/layout/Sidebar/Bar.tsx index a315406b58..c86f60a0a2 100644 --- a/packages/core-components/src/layout/Sidebar/Bar.tsx +++ b/packages/core-components/src/layout/Sidebar/Bar.tsx @@ -32,7 +32,7 @@ import { } from './config'; import { BackstageTheme } from '@backstage/theme'; import { useContent } from './Page'; -import { SidebarContextProvider } from './SidebarContext'; +import { SidebarOpenStateProvider } from './SidebarOpenStateContext'; import { useSidebarPinState } from './SidebarPinStateContext'; import { MobileSidebar } from './MobileSidebar'; @@ -190,7 +190,7 @@ const DesktopSidebar = (props: DesktopSidebarProps) => { return (
-
+
); }; diff --git a/packages/core-components/src/layout/Sidebar/Intro.tsx b/packages/core-components/src/layout/Sidebar/Intro.tsx index 51d453b22d..05d580c960 100644 --- a/packages/core-components/src/layout/Sidebar/Intro.tsx +++ b/packages/core-components/src/layout/Sidebar/Intro.tsx @@ -28,7 +28,7 @@ import { SIDEBAR_INTRO_LOCAL_STORAGE, } from './config'; import { SidebarDivider } from './Items'; -import { useSidebar } from './SidebarContext'; +import { useSidebarOpenState } from './SidebarOpenStateContext'; /** @public */ export type SidebarIntroClassKey = @@ -151,7 +151,7 @@ const recentlyViewedIntroText = 'And your recently viewed plugins will pop up here!'; export function SidebarIntro(_props: {}) { - const { isOpen } = useSidebar(); + const { isOpen } = useSidebarOpenState(); const defaultValue = { starredItemsDismissed: false, recentlyViewedItemsDismissed: false, diff --git a/packages/core-components/src/layout/Sidebar/Items.tsx b/packages/core-components/src/layout/Sidebar/Items.tsx index 3d294a54a8..0413864084 100644 --- a/packages/core-components/src/layout/Sidebar/Items.tsx +++ b/packages/core-components/src/layout/Sidebar/Items.tsx @@ -61,7 +61,7 @@ import DoubleArrowLeft from './icons/DoubleArrowLeft'; import DoubleArrowRight from './icons/DoubleArrowRight'; import { isLocationMatch } from './utils'; import { Location } from 'history'; -import { useSidebar } from './SidebarContext'; +import { useSidebarOpenState } from './SidebarOpenStateContext'; /** @public */ export type SidebarItemClassKey = @@ -369,7 +369,7 @@ const SidebarItemBase = forwardRef((props, ref) => { // XXX (@koroeskohr): unsure this is optimal. But I just really didn't want to have the item component // depend on the current location, and at least have it being optionally forced to selected. // Still waiting on a Q answered to fine tune the implementation - const { isOpen } = useSidebar(); + const { isOpen } = useSidebarOpenState(); const divStyle = !isOpen && hasSubmenu ? { display: 'flex', marginLeft: '24px' } : {}; @@ -671,7 +671,7 @@ export const SidebarScrollWrapper = styled('div')(({ theme }) => { export const SidebarExpandButton = () => { const { sidebarConfig } = useContext(SidebarConfigContext); const classes = useMemoStyles(sidebarConfig); - const { isOpen, setOpen } = useSidebar(); + const { isOpen, setOpen } = useSidebarOpenState(); const isSmallScreen = useMediaQuery( theme => theme.breakpoints.down('md'), { noSsr: true }, diff --git a/packages/core-components/src/layout/Sidebar/MobileSidebar.tsx b/packages/core-components/src/layout/Sidebar/MobileSidebar.tsx index 46f38d9a2c..b51e3cbc0d 100644 --- a/packages/core-components/src/layout/Sidebar/MobileSidebar.tsx +++ b/packages/core-components/src/layout/Sidebar/MobileSidebar.tsx @@ -27,7 +27,7 @@ import MenuIcon from '@material-ui/icons/Menu'; import { orderBy } from 'lodash'; import React, { createContext, useEffect, useState, useContext } from 'react'; import { useLocation } from 'react-router'; -import { SidebarContextProvider } from './SidebarContext'; +import { SidebarOpenStateProvider } from './SidebarOpenStateContext'; import { SidebarGroup } from './SidebarGroup'; import { SidebarConfigContext, SidebarConfig } from './config'; @@ -208,7 +208,7 @@ export const MobileSidebar = (props: MobileSidebarProps) => { !sidebarGroups[selectedMenuItemIndex].props.to; return ( - {} }}> + {} }}> @@ -232,6 +232,6 @@ export const MobileSidebar = (props: MobileSidebarProps) => { {sidebarGroups} - +
); }; diff --git a/packages/core-components/src/layout/Sidebar/Page.tsx b/packages/core-components/src/layout/Sidebar/Page.tsx index 17cd5ddee1..bdf41b5dd7 100644 --- a/packages/core-components/src/layout/Sidebar/Page.tsx +++ b/packages/core-components/src/layout/Sidebar/Page.tsx @@ -29,7 +29,7 @@ import { SidebarConfigContext, SidebarConfig } from './config'; import { BackstageTheme } from '@backstage/theme'; import { LocalStorage } from './localStorage'; import useMediaQuery from '@material-ui/core/useMediaQuery'; -import { SidebarPinStateContextProvider } from './SidebarPinStateContext'; +import { SidebarPinStateProvider } from './SidebarPinStateContext'; export type SidebarPageClassKey = 'root'; @@ -114,7 +114,7 @@ export function SidebarPage(props: SidebarPageProps) { const classes = useStyles({ isPinned, sidebarConfig }); return ( -
{props.children}
-
+ ); } diff --git a/packages/core-components/src/layout/Sidebar/SidebarContext.test.tsx b/packages/core-components/src/layout/Sidebar/SidebarOpenStateContext.test.tsx similarity index 78% rename from packages/core-components/src/layout/Sidebar/SidebarContext.test.tsx rename to packages/core-components/src/layout/Sidebar/SidebarOpenStateContext.test.tsx index 43a18851d7..4953e1341b 100644 --- a/packages/core-components/src/layout/Sidebar/SidebarContext.test.tsx +++ b/packages/core-components/src/layout/Sidebar/SidebarOpenStateContext.test.tsx @@ -19,17 +19,17 @@ import { waitFor } from '@testing-library/react'; import { renderHook, act } from '@testing-library/react-hooks'; import { LegacySidebarContext, - SidebarContextProvider, - useSidebar, -} from './SidebarContext'; + SidebarOpenStateProvider, + useSidebarOpenState, +} from './SidebarOpenStateContext'; -describe('SidebarContext', () => { - describe('SidebarContextProvider', () => { +describe('SidebarOpenStateContext', () => { + describe('SidebarOpenStateProvider', () => { it('should render children', async () => { const { findByText } = await renderWithEffects( - {} }}> + {} }}> Child - , + , ); expect(await findByText('Child')).toBeInTheDocument(); }); @@ -41,23 +41,23 @@ describe('SidebarContext', () => { }; const { findByText } = await renderWithEffects( - {}, }} > - , + , ); expect(await findByText('true')).toBeInTheDocument(); }); }); - describe('useSidebar', () => { + describe('useSidebarOpenState', () => { it('does not need to be invoked within provider', () => { - const { result } = renderHook(() => useSidebar()); + const { result } = renderHook(() => useSidebarOpenState()); expect(result.current.isOpen).toBe(false); expect(typeof result.current.setOpen).toBe('function'); }); @@ -65,7 +65,7 @@ describe('SidebarContext', () => { it('should read and update state', async () => { let actualValue = true; const wrapper = ({ children }: { children: ReactNode }) => ( - { @@ -74,9 +74,11 @@ describe('SidebarContext', () => { }} > {children} - + ); - const { result, rerender } = renderHook(() => useSidebar(), { wrapper }); + const { result, rerender } = renderHook(() => useSidebarOpenState(), { + wrapper, + }); expect(result.current.isOpen).toBe(true); diff --git a/packages/core-components/src/layout/Sidebar/SidebarContext.tsx b/packages/core-components/src/layout/Sidebar/SidebarOpenStateContext.tsx similarity index 79% rename from packages/core-components/src/layout/Sidebar/SidebarContext.tsx rename to packages/core-components/src/layout/Sidebar/SidebarOpenStateContext.tsx index 4e47b6040a..1ceb6d95cf 100644 --- a/packages/core-components/src/layout/Sidebar/SidebarContext.tsx +++ b/packages/core-components/src/layout/Sidebar/SidebarOpenStateContext.tsx @@ -23,13 +23,24 @@ import { /** * Types for the `SidebarContext` * - * @public + * @public @deprecated + * Use `SidebarOpenState` instead. */ export type SidebarContextType = { isOpen: boolean; setOpen: (open: boolean) => void; }; +/** + * The open state of the sidebar. + * + * @public + */ +export type SidebarOpenState = { + isOpen: boolean; + setOpen: (open: boolean) => void; +}; + const defaultSidebarContext = { isOpen: false, setOpen: () => {}, @@ -46,20 +57,20 @@ export const LegacySidebarContext = createContext( ); const VersionedSidebarContext = createVersionedContext<{ - 1: SidebarContextType; -}>('sidebar-context'); + 1: SidebarOpenState; +}>('sidebar-open-state-context'); /** * Provides context for reading and updating sidebar state. * * @public */ -export const SidebarContextProvider = ({ +export const SidebarOpenStateProvider = ({ children, value, }: { children: ReactNode; - value: SidebarContextType; + value: SidebarOpenState; }) => ( { +export const useSidebarOpenState = (): SidebarOpenState => { const versionedSidebarContext = useContext(VersionedSidebarContext); - // Invoked from outside a SidbarContextProvider, return a default value. + // Invoked from outside a SidebarOpenStateProvider, return a default value. if (versionedSidebarContext === undefined) { return defaultSidebarContext; } diff --git a/packages/core-components/src/layout/Sidebar/SidebarPinStateContext.test.tsx b/packages/core-components/src/layout/Sidebar/SidebarPinStateContext.test.tsx index d8bc054e4a..1a86ec612e 100644 --- a/packages/core-components/src/layout/Sidebar/SidebarPinStateContext.test.tsx +++ b/packages/core-components/src/layout/Sidebar/SidebarPinStateContext.test.tsx @@ -19,15 +19,15 @@ import { waitFor } from '@testing-library/react'; import { renderHook, act } from '@testing-library/react-hooks'; import { LegacySidebarPinStateContext, - SidebarPinStateContextProvider, + SidebarPinStateProvider, useSidebarPinState, } from './SidebarPinStateContext'; describe('SidebarPinStateContext', () => { - describe('SidebarPinStateContextProvider', () => { + describe('SidebarPinStateProvider', () => { it('should render children', async () => { const { findByText } = await renderWithEffects( - { }} > Child - , + , ); expect(await findByText('Child')).toBeInTheDocument(); }); @@ -47,7 +47,7 @@ describe('SidebarPinStateContext', () => { }; const { findByText } = await renderWithEffects( - { }} > - , + , ); expect(await findByText('true')).toBeInTheDocument(); @@ -73,7 +73,7 @@ describe('SidebarPinStateContext', () => { it('should read and update state', async () => { let actualValue = true; const wrapper = ({ children }: { children: ReactNode }) => ( - { }} > {children} - + ); const { result, rerender } = renderHook(() => useSidebarPinState(), { wrapper, diff --git a/packages/core-components/src/layout/Sidebar/SidebarPinStateContext.tsx b/packages/core-components/src/layout/Sidebar/SidebarPinStateContext.tsx index ec38884890..0eb59dd119 100644 --- a/packages/core-components/src/layout/Sidebar/SidebarPinStateContext.tsx +++ b/packages/core-components/src/layout/Sidebar/SidebarPinStateContext.tsx @@ -22,7 +22,8 @@ import React, { createContext, ReactNode, useContext } from 'react'; /** * Type of `SidebarPinStateContext` * - * @public + * @public @deprecated + * Use `SidebarPinState` instead. */ export type SidebarPinStateContextType = { isPinned: boolean; @@ -30,6 +31,17 @@ export type SidebarPinStateContextType = { isMobile?: boolean; }; +/** + * The pin state of the sidebar. + * + * @public + */ +export type SidebarPinState = { + isPinned: boolean; + toggleSidebarPinState: () => any; + isMobile?: boolean; +}; + const defaultSidebarPinStateContext = { isPinned: true, toggleSidebarPinState: () => {}, @@ -46,7 +58,7 @@ export const LegacySidebarPinStateContext = createContext(defaultSidebarPinStateContext); const VersionedSidebarPinStateContext = createVersionedContext<{ - 1: SidebarPinStateContextType; + 1: SidebarPinState; }>('sidebar-pin-state-context'); /** @@ -54,7 +66,7 @@ const VersionedSidebarPinStateContext = createVersionedContext<{ * * @public */ -export const SidebarPinStateContextProvider = ({ +export const SidebarPinStateProvider = ({ children, value, }: { @@ -75,10 +87,10 @@ export const SidebarPinStateContextProvider = ({ * * @public */ -export const useSidebarPinState = (): SidebarPinStateContextType => { +export const useSidebarPinState = (): SidebarPinState => { const versionedSidebarContext = useContext(VersionedSidebarPinStateContext); - // Invoked from outside a SidebarPinStateContextProvider: default value. + // Invoked from outside a SidebarPinStateProvider: default value. if (versionedSidebarContext === undefined) { return defaultSidebarPinStateContext; } diff --git a/packages/core-components/src/layout/Sidebar/SidebarSubmenu.tsx b/packages/core-components/src/layout/Sidebar/SidebarSubmenu.tsx index d8a87c98d9..438e580ef4 100644 --- a/packages/core-components/src/layout/Sidebar/SidebarSubmenu.tsx +++ b/packages/core-components/src/layout/Sidebar/SidebarSubmenu.tsx @@ -22,7 +22,7 @@ import { SidebarConfigContext, SubmenuConfig, } from './config'; -import { useSidebar } from './SidebarContext'; +import { useSidebarOpenState } from './SidebarOpenStateContext'; import { BackstageTheme } from '@backstage/theme'; const useStyles = makeStyles< @@ -105,7 +105,7 @@ export type SidebarSubmenuProps = { * @public */ export const SidebarSubmenu = (props: SidebarSubmenuProps) => { - const { isOpen } = useSidebar(); + const { isOpen } = useSidebarOpenState(); const { sidebarConfig, submenuConfig } = useContext(SidebarConfigContext); const left = isOpen ? sidebarConfig.drawerWidthOpen diff --git a/packages/core-components/src/layout/Sidebar/index.ts b/packages/core-components/src/layout/Sidebar/index.ts index 06681ded52..4c91ea6a8f 100644 --- a/packages/core-components/src/layout/Sidebar/index.ts +++ b/packages/core-components/src/layout/Sidebar/index.ts @@ -50,13 +50,19 @@ export { SIDEBAR_INTRO_LOCAL_STORAGE, sidebarConfig } from './config'; export type { SidebarOptions, SubmenuOptions } from './config'; export { LegacySidebarContext as SidebarContext, - SidebarContextProvider, - useSidebar, -} from './SidebarContext'; -export type { SidebarContextType } from './SidebarContext'; + SidebarOpenStateProvider, + useSidebarOpenState, +} from './SidebarOpenStateContext'; +export type { + SidebarContextType, + SidebarOpenState, +} from './SidebarOpenStateContext'; export { LegacySidebarPinStateContext as SidebarPinStateContext, - SidebarPinStateContextProvider, + SidebarPinStateProvider, useSidebarPinState, } from './SidebarPinStateContext'; -export type { SidebarPinStateContextType } from './SidebarPinStateContext'; +export type { + SidebarPinStateContextType, + SidebarPinState, +} from './SidebarPinStateContext'; diff --git a/packages/create-app/templates/default-app/packages/app/src/components/Root/Root.tsx b/packages/create-app/templates/default-app/packages/app/src/components/Root/Root.tsx index 05e11458ce..b1164a32f0 100644 --- a/packages/create-app/templates/default-app/packages/app/src/components/Root/Root.tsx +++ b/packages/create-app/templates/default-app/packages/app/src/components/Root/Root.tsx @@ -38,7 +38,7 @@ import { SidebarPage, SidebarScrollWrapper, SidebarSpace, - useSidebar, + useSidebarOpenState, } from '@backstage/core-components'; import MenuIcon from '@material-ui/icons/Menu'; import SearchIcon from '@material-ui/icons/Search'; @@ -60,7 +60,7 @@ const useSidebarLogoStyles = makeStyles({ const SidebarLogo = () => { const classes = useSidebarLogoStyles(); - const { isOpen } = useSidebar(); + const { isOpen } = useSidebarOpenState(); return (
diff --git a/packages/techdocs-cli-embedded-app/src/components/Root/Root.tsx b/packages/techdocs-cli-embedded-app/src/components/Root/Root.tsx index 6613e7c0ea..cd8b5d35ed 100644 --- a/packages/techdocs-cli-embedded-app/src/components/Root/Root.tsx +++ b/packages/techdocs-cli-embedded-app/src/components/Root/Root.tsx @@ -27,7 +27,7 @@ import { SidebarPage, sidebarConfig, SidebarDivider, - useSidebar, + useSidebarOpenState, } from '@backstage/core-components'; import { NavLink } from 'react-router-dom'; @@ -48,7 +48,7 @@ const useSidebarLogoStyles = makeStyles({ const SidebarLogo = () => { const classes = useSidebarLogoStyles(); - const { isOpen } = useSidebar(); + const { isOpen } = useSidebarOpenState(); return (
diff --git a/plugins/shortcuts/src/ShortcutItem.test.tsx b/plugins/shortcuts/src/ShortcutItem.test.tsx index d1a1018513..b98dd2dc0b 100644 --- a/plugins/shortcuts/src/ShortcutItem.test.tsx +++ b/plugins/shortcuts/src/ShortcutItem.test.tsx @@ -20,7 +20,7 @@ import { ShortcutItem } from './ShortcutItem'; import { Shortcut } from './types'; import { LocalStoredShortcuts } from './api'; import { MockStorageApi, renderInTestApp } from '@backstage/test-utils'; -import { SidebarContextProvider } from '@backstage/core-components'; +import { SidebarOpenStateProvider } from '@backstage/core-components'; describe('ShortcutItem', () => { const shortcut: Shortcut = { @@ -32,9 +32,9 @@ describe('ShortcutItem', () => { it('displays the shortcut', async () => { await renderInTestApp( - {} }}> + {} }}> - , + , ); expect(screen.getByText('ST')).toBeInTheDocument(); expect(screen.getByText('some title')).toBeInTheDocument(); diff --git a/plugins/shortcuts/src/Shortcuts.test.tsx b/plugins/shortcuts/src/Shortcuts.test.tsx index 813dbb8def..02b351d3a4 100644 --- a/plugins/shortcuts/src/Shortcuts.test.tsx +++ b/plugins/shortcuts/src/Shortcuts.test.tsx @@ -24,12 +24,12 @@ import { screen, waitFor } from '@testing-library/react'; import { Shortcuts } from './Shortcuts'; import { LocalStoredShortcuts, shortcutsApiRef } from './api'; -import { SidebarContextProvider } from '@backstage/core-components'; +import { SidebarOpenStateProvider } from '@backstage/core-components'; describe('Shortcuts', () => { it('displays an add button', async () => { await renderInTestApp( - {} }}> + {} }}> { > - , + , ); await waitFor(() => !screen.queryByTestId('progress')); expect(screen.getByText('Add Shortcuts')).toBeInTheDocument(); diff --git a/plugins/user-settings/src/components/General/UserSettingsPinToggle.test.tsx b/plugins/user-settings/src/components/General/UserSettingsPinToggle.test.tsx index a41792768a..ef8046a54e 100644 --- a/plugins/user-settings/src/components/General/UserSettingsPinToggle.test.tsx +++ b/plugins/user-settings/src/components/General/UserSettingsPinToggle.test.tsx @@ -18,14 +18,14 @@ import { renderWithEffects, wrapInTestApp } from '@backstage/test-utils'; import { fireEvent } from '@testing-library/react'; import React from 'react'; import { UserSettingsPinToggle } from './UserSettingsPinToggle'; -import { SidebarPinStateContextProvider } from '@backstage/core-components'; +import { SidebarPinStateProvider } from '@backstage/core-components'; describe('', () => { it('toggles the pin sidebar button', async () => { const mockToggleFn = jest.fn(); const rendered = await renderWithEffects( wrapInTestApp( - ', () => { }} > - , + , ), ); expect(rendered.getByText('Pin Sidebar')).toBeInTheDocument(); From 58a957f4fc6faafc47e8f890e7aaad3b0ea1ff73 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Mon, 23 May 2022 14:37:39 +0200 Subject: [PATCH 06/22] Better document hooks, props, etc. Signed-off-by: Eric Peterson --- .../layout/Sidebar/SidebarOpenStateContext.tsx | 12 +++++++++++- .../layout/Sidebar/SidebarPinStateContext.tsx | 17 ++++++++++++++++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/packages/core-components/src/layout/Sidebar/SidebarOpenStateContext.tsx b/packages/core-components/src/layout/Sidebar/SidebarOpenStateContext.tsx index 1ceb6d95cf..be15785f29 100644 --- a/packages/core-components/src/layout/Sidebar/SidebarOpenStateContext.tsx +++ b/packages/core-components/src/layout/Sidebar/SidebarOpenStateContext.tsx @@ -37,7 +37,16 @@ export type SidebarContextType = { * @public */ export type SidebarOpenState = { + /** + * Whether or not the sidebar is open and full-width. When `false`, the + * sidebar is "closed" and typically only shows icons with no text. + */ isOpen: boolean; + + /** + * A function to set whether or not the sidebar is open. Pass `true` to open + * the sidebar. Pass `false` to close it. + */ setOpen: (open: boolean) => void; }; @@ -82,7 +91,8 @@ export const SidebarOpenStateProvider = ({ ); /** - * Hook to read and update the sidebar's open state. + * Hook to read and update the sidebar's open state, which controls whether or + * not the sidebar is open and full-width, or closed and only displaying icons. * * @public */ diff --git a/packages/core-components/src/layout/Sidebar/SidebarPinStateContext.tsx b/packages/core-components/src/layout/Sidebar/SidebarPinStateContext.tsx index 0eb59dd119..a85929ab70 100644 --- a/packages/core-components/src/layout/Sidebar/SidebarPinStateContext.tsx +++ b/packages/core-components/src/layout/Sidebar/SidebarPinStateContext.tsx @@ -37,8 +37,22 @@ export type SidebarPinStateContextType = { * @public */ export type SidebarPinState = { + /** + * Whether or not the sidebar is pinned to the `open` state. When `isPinned` + * is `false`, the sidebar opens and closes on hover. When `true`, the + * sidebar is permanently opened, regardless of user interaction. + */ isPinned: boolean; + + /** + * A function to toggle the pin state of the sidebar. + */ toggleSidebarPinState: () => any; + + /** + * Whether or not the sidebar is or should be rendered in a mobile-optimized + * way. + */ isMobile?: boolean; }; @@ -83,7 +97,8 @@ export const SidebarPinStateProvider = ({ ); /** - * Hook to read and update sidebar pin state. + * Hook to read and update sidebar pin state, which controls whether or not the + * sidebar is pinned open. * * @public */ From 09d2f4d1794738c663fda9bca9daf017ab317bb9 Mon Sep 17 00:00:00 2001 From: Leon Date: Tue, 24 May 2022 14:43:07 +0200 Subject: [PATCH 07/22] export TechInsightsClient Signed-off-by: Leon --- .changeset/heavy-carrots-cheer.md | 5 +++++ plugins/tech-insights/src/api/index.ts | 1 + plugins/tech-insights/src/index.ts | 5 ++--- 3 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 .changeset/heavy-carrots-cheer.md diff --git a/.changeset/heavy-carrots-cheer.md b/.changeset/heavy-carrots-cheer.md new file mode 100644 index 0000000000..484e04d8bd --- /dev/null +++ b/.changeset/heavy-carrots-cheer.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-tech-insights': patch +--- + +Export TechInsightsClient so it may be extended by custom implementations diff --git a/plugins/tech-insights/src/api/index.ts b/plugins/tech-insights/src/api/index.ts index bcd2575a52..9f5545e671 100644 --- a/plugins/tech-insights/src/api/index.ts +++ b/plugins/tech-insights/src/api/index.ts @@ -15,3 +15,4 @@ */ export * from './TechInsightsApi'; export * from './TechInsightsClient'; +export * from './types'; diff --git a/plugins/tech-insights/src/index.ts b/plugins/tech-insights/src/index.ts index bc7c2ce26c..97bdc95631 100644 --- a/plugins/tech-insights/src/index.ts +++ b/plugins/tech-insights/src/index.ts @@ -19,7 +19,6 @@ export { EntityTechInsightsScorecardCard, } from './plugin'; -export { techInsightsApiRef } from './api/TechInsightsApi'; -export type { TechInsightsApi } from './api/TechInsightsApi'; -export type { Check } from './api/types'; +export { techInsightsApiRef, TechInsightsClient } from './api'; +export type { TechInsightsApi, Check } from './api'; export type { CheckResultRenderer } from './components/CheckResultRenderer'; From 65e760664e78ee448f168da214d58af7efabc917 Mon Sep 17 00:00:00 2001 From: Leon Date: Tue, 24 May 2022 14:49:59 +0200 Subject: [PATCH 08/22] removed explicit type because it became a warning in api-report.md Signed-off-by: Leon --- plugins/tech-insights/src/api/TechInsightsClient.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/plugins/tech-insights/src/api/TechInsightsClient.ts b/plugins/tech-insights/src/api/TechInsightsClient.ts index 04de46dba9..899e000695 100644 --- a/plugins/tech-insights/src/api/TechInsightsClient.ts +++ b/plugins/tech-insights/src/api/TechInsightsClient.ts @@ -29,16 +29,14 @@ import { defaultCheckResultRenderers, } from '../components/CheckResultRenderer'; -export type Options = { - discoveryApi: DiscoveryApi; - identityApi: IdentityApi; -}; - export class TechInsightsClient implements TechInsightsApi { private readonly discoveryApi: DiscoveryApi; private readonly identityApi: IdentityApi; - constructor(options: Options) { + constructor(options: { + discoveryApi: DiscoveryApi; + identityApi: IdentityApi; + }) { this.discoveryApi = options.discoveryApi; this.identityApi = options.identityApi; } From 23bf9593d75c4fefe3df07c8be848ea015f9062c Mon Sep 17 00:00:00 2001 From: Leon Date: Tue, 24 May 2022 14:50:15 +0200 Subject: [PATCH 09/22] generate api-report Signed-off-by: Leon --- plugins/tech-insights/api-report.md | 31 +++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/plugins/tech-insights/api-report.md b/plugins/tech-insights/api-report.md index 3e099493b6..41e37ca4ff 100644 --- a/plugins/tech-insights/api-report.md +++ b/plugins/tech-insights/api-report.md @@ -10,6 +10,8 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { BulkCheckResponse } from '@backstage/plugin-tech-insights-common'; import { CheckResult } from '@backstage/plugin-tech-insights-common'; import { CompoundEntityRef } from '@backstage/catalog-model'; +import { DiscoveryApi } from '@backstage/core-plugin-api'; +import { IdentityApi } from '@backstage/core-plugin-api'; import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; @@ -78,6 +80,35 @@ export interface TechInsightsApi { // @public export const techInsightsApiRef: ApiRef; +// Warning: (ae-missing-release-tag) "TechInsightsClient" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export class TechInsightsClient implements TechInsightsApi { + constructor(options: { + discoveryApi: DiscoveryApi; + identityApi: IdentityApi; + }); + // (undocumented) + getAllChecks(): Promise; + // (undocumented) + getScorecardsDefinition( + type: string, + value: CheckResult[], + title?: string, + description?: string, + ): CheckResultRenderer | undefined; + // (undocumented) + runBulkChecks( + entities: CompoundEntityRef[], + checks?: Check[], + ): Promise; + // (undocumented) + runChecks( + entityParams: CompoundEntityRef, + checks?: string[], + ): Promise; +} + // @public (undocumented) export const techInsightsPlugin: BackstagePlugin< { From 6107616ad8d4c0e6c9370f767dec0c592d9ed383 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Thu, 26 May 2022 17:24:01 +0200 Subject: [PATCH 10/22] Adding a guide for integrating search into existing plugins. Signed-off-by: Eric Peterson --- .../features/search/search-for-plugin-devs.md | 187 ++++++++++++++++++ microsite/sidebars.json | 3 +- 2 files changed, 189 insertions(+), 1 deletion(-) create mode 100644 docs/features/search/search-for-plugin-devs.md diff --git a/docs/features/search/search-for-plugin-devs.md b/docs/features/search/search-for-plugin-devs.md new file mode 100644 index 0000000000..d96b05c6c5 --- /dev/null +++ b/docs/features/search/search-for-plugin-devs.md @@ -0,0 +1,187 @@ +--- +id: for-plugin-devs +title: Search for Plugin Developers +description: How to integrate search into a Backstage plugin +--- + +The Backstage Search Platform was designed to give plugin developers the APIs +and interfaces needed to offer search experiences within their plugins, while +abstracting away (and instead empowering application integrators to choose) the +specific underlying search technologies. + +On this page, you'll find concepts, guides, and recipes for how to leverage the +Backstage Search Platform in your plugin. + +## Providing data to the search platform + +> A guide on how to create collators is coming soon! + +## Building a search experience into your plugin + +While the core Search plugin offers components and extensions that empower app +integrators to compose a global search experience, you may find that you want a +narrower search experience just within your plugin. This could be as literal as +an autocomplete-style search bar focused on documents provided by your plugin, +or as abstract as a widget that presents a list of links that relate in some +way to something else on the page. + +### Concepts + +Knowing these high-level concepts will help you as you craft your in-plugin +search experience. + +- All search experiences must be wrapped in a ``, which + is provided by `@backstage/plugin-search-react`. This context keeps track + of state necessary to perform search queries and display any results. As + inputs to the query are updated (e.g. a `term` or `filter` values), the + updated query is executed and `results` are refreshed. +- The aforementioned state can be modified and/or consumed via the + `useSearch()` hook, also exported by `@backstage/plugin-search-react`. +- For more literal search experiences, reusable components are available + to import and compose into a cohesive experience in your plugin (e.g. + `` or ``). You can see all such + components in [Backstage's storybook](https://backstage.io/storybook/?path=/story/plugins-search-searchbar--default). + +### Recipes + +#### Improved "404" page experience + +Imagine you have a plugin that allows users to manage _widgets_. Perhaps they +can be viewed at a URL like `backstage.example.biz/widgets/{widgetName}`. +At some point, a widget is renamed, and links to that widget's page from +chat systems, wikis, or browser bookmarks become stale, resulting in errors or +404s. + +What if instead of showing a broken page or the generic "looks like someone +dropped the mic" 404 page, you showed a list of possibly related widgets? + +```javascript +import { Link } from '@backstage/core-components'; +import { SearchResult } from '@backstage/plugin-search'; +import { SearchContextProvider } from '@backstage/plugin-search-react'; + +export const Widget404Page = ({ widgetName }) => { + // Supplying this to runs a pre-filtered search with + // the given widgetName as the search term, focused on search result of type + // "widget" with no other filters. + const preFiltered = { + term: widgetName, + types: ['widget'], + filters: {}, + }; + + return ( + + {/* The component allows us to iterate through results and + display them in whatever way fits best! */} + + {({ results }) => ( + {results.map(({ document }) => ( + + {document.title} + + ))} + )} + + + ); +); +``` + +Not all search experiences require user input! As you can see, it's possible to +leverage the Backstage Search Platform's frontend framework without necessarily +giving users input controls. + +#### Simple search page + +Of course, it's also possible to provide a more fully featured search +experience in your plugin. The simplest way is to leverage reusable components +provided by the `@backstage/plugin-search` package, like this: + +```javascript +import { useProfile } from '@internal/api'; +import { + Content, + ContentHeader, + PageWithHeader, +} from '@backstage/core-components'; +import { SearchBar, SearchResult } from '@backstage/plugin-search'; +import { SearchContextProvider } from '@backstage/plugin-search-react'; + +export const ManageMyWidgets = () => { + const { primaryTeam } = useProfile(); + // In this example, note how we are pre-filtering results down to a specific + // owner field value (the currently logged-in user's team), but allowing the + // search term to be controlled by the user via the component. + const preFiltered = { + types: ['widget'], + term: '', + filters: { + owner: primaryTeam, + }, + }; + + return ( + + + + + + + {/* Render results here, just like above */} + + + + + ); +}; +``` + +#### Custom search control surfaces + +If the reusable search components provided by `@backstage/plugin-search` aren't +adequate, no problem! There's an API in place that you can use to author your +own components to control the various parts of the search context. + +```javascript +import { useSearch } from '@backstage/plugin-search-react'; +import ChipInput from 'material-ui-chip-input'; + +export const CustomChipFilter = ({ name }) => { + const { filters, setFilters } = useSearch(); + const chipValues = filters[name] || []; + + // When a chip value is changed, update the filters value by calling the + // setFilters function from the search context. + const handleChipChange = (chip, index) => { + // There may be filters set for other fields. Be sure to maintain them. + setFilters(prevState => { + const { [name]: filter = [], ...others } = prevState; + + if (index === undefined) { + filter.push(chip); + } else { + filter.splice(index, 1); + } + + return { ...others, [name]: filter }; + }); + }; + + return ( + + ); +}; +``` + +Check out the [SearchContextValue type](https://github.com/backstage/backstage/blob/master/plugins/search-react/src/context/SearchContext.tsx) +for more details on what methods and values are available for manipulating and +reading the search context. + +If you produce something generic and reusable, consider contributing your +component upstream so that all users of the Backstage Search Platform can +benefit. Issues and pull requests welcome. diff --git a/microsite/sidebars.json b/microsite/sidebars.json index a8dc4e53ee..e85eba8ba1 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -99,7 +99,8 @@ "features/search/concepts", "features/search/architecture", "features/search/search-engines", - "features/search/how-to-guides" + "features/search/how-to-guides", + "features/search/for-plugin-devs" ] }, { From 9387626eeccef35f5a611aa256a5d6d02108e6c1 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Fri, 27 May 2022 13:23:05 +0200 Subject: [PATCH 11/22] Clearer, more precise language. Intro packages used. Signed-off-by: Eric Peterson --- .../features/search/search-for-plugin-devs.md | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/docs/features/search/search-for-plugin-devs.md b/docs/features/search/search-for-plugin-devs.md index d96b05c6c5..70697cd91c 100644 --- a/docs/features/search/search-for-plugin-devs.md +++ b/docs/features/search/search-for-plugin-devs.md @@ -9,8 +9,8 @@ and interfaces needed to offer search experiences within their plugins, while abstracting away (and instead empowering application integrators to choose) the specific underlying search technologies. -On this page, you'll find concepts, guides, and recipes for how to leverage the -Backstage Search Platform in your plugin. +On this page, you'll find concepts and tutorials for leveraging the Backstage +Search Platform in your plugin. ## Providing data to the search platform @@ -21,11 +21,12 @@ Backstage Search Platform in your plugin. While the core Search plugin offers components and extensions that empower app integrators to compose a global search experience, you may find that you want a narrower search experience just within your plugin. This could be as literal as -an autocomplete-style search bar focused on documents provided by your plugin, -or as abstract as a widget that presents a list of links that relate in some -way to something else on the page. +an autocomplete-style search bar focused on documents provided by your plugin +(for example, the [TechDocsSearch](https://github.com/backstage/backstage/blob/master/plugins/techdocs/src/search/components/TechDocsSearch.tsx) +component), or as abstract as a widget that presents a list of links that +are contextually related to something else on the page. -### Concepts +### Search Experience Concepts Knowing these high-level concepts will help you as you craft your in-plugin search experience. @@ -34,7 +35,9 @@ search experience. is provided by `@backstage/plugin-search-react`. This context keeps track of state necessary to perform search queries and display any results. As inputs to the query are updated (e.g. a `term` or `filter` values), the - updated query is executed and `results` are refreshed. + updated query is executed and `results` are refreshed. Check out the + [SearchContextValue](https://backstage.io/docs/reference/plugin-search-react.searchcontextvalue) + for details. - The aforementioned state can be modified and/or consumed via the `useSearch()` hook, also exported by `@backstage/plugin-search-react`. - For more literal search experiences, reusable components are available @@ -42,7 +45,20 @@ search experience. `` or ``). You can see all such components in [Backstage's storybook](https://backstage.io/storybook/?path=/story/plugins-search-searchbar--default). -### Recipes +### Search Experience Tutorials + +The following tutorials make use of packages and plugins that you may not yet +have as dependencies for your plugin; be sure to add them before you use them! + +- [`@backstage/plugin-search-react`](https://www.npmjs.com/package/@backstage/plugin-search-react) - A + package containing components, hooks, and types that are shared across all + frontend plugins, including plugins like yours! +- [`@backstage/plugin-search`](https://www.npmjs.com/package/@backstage/plugin-search) - The + main search plugin, used by app integrators to compose global search + experiences. +- [`@backstage/core-components`](https://www.npmjs.com/package/@backstage/core-components) - A + package containing generic components useful for a variety of experiences + built in Backstage. #### Improved "404" page experience From 6b33e49261c7310a76542d39993e383c201f8429 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 30 May 2022 12:41:04 +0000 Subject: [PATCH 12/22] chore(deps): update dependency lint-staged to v12.4.3 Signed-off-by: Renovate Bot --- yarn.lock | 47 +++++++++++++++++++++++++++++++---------------- 1 file changed, 31 insertions(+), 16 deletions(-) diff --git a/yarn.lock b/yarn.lock index 245c2158bb..15ef2cf1bf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9491,6 +9491,11 @@ commander@^9.1.0: resolved "https://registry.npmjs.org/commander/-/commander-9.2.0.tgz#6e21014b2ed90d8b7c9647230d8b7a94a4a419a9" integrity sha512-e2i4wANQiSXgnrBlIatyHtP1odfUp0BbV5Y5nEGbxtIrStkEOAAzCUirvLBNXHLr7kwLvJl6V+4V3XV9x7Wd9w== +commander@^9.3.0: + version "9.3.0" + resolved "https://registry.npmjs.org/commander/-/commander-9.3.0.tgz#f619114a5a2d2054e0d9ff1b31d5ccf89255e26b" + integrity sha512-hv95iU5uXPbK83mjrJKuZyFM/LBAoCV/XhVGkS5Je6tl7sxr6A0ITMw5WoRV46/UaJ46Nllm3Xt7IaJhXTIkzw== + common-ancestor-path@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/common-ancestor-path/-/common-ancestor-path-1.0.1.tgz#4f7d2d1394d91b7abdf51871c62f71eadb0182a7" @@ -15089,7 +15094,7 @@ isbinaryfile@^5.0.0: isexe@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== isobject@^2.0.0: version "2.1.0" @@ -16498,7 +16503,12 @@ libnpmpublish@^4.0.0: semver "^7.1.3" ssri "^8.0.0" -lilconfig@2.0.4, lilconfig@^2.0.3: +lilconfig@2.0.5: + version "2.0.5" + resolved "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.5.tgz#19e57fd06ccc3848fd1891655b5a447092225b25" + integrity sha512-xaYmXZtTHPAw5m+xLN8ab9C+3a8YmV3asNSPOATITbtwrfbwaLJj8h66H1WMIpALCkqsIzK3h7oQ+PdX+LQ9Eg== + +lilconfig@^2.0.3: version "2.0.4" resolved "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.4.tgz#f4507d043d7058b380b6a8f5cb7bcd4b34cee082" integrity sha512-bfTIN7lEsiooCocSISTWXkiWJkRqtL9wYtYy+8EK3Y41qh3mpwPU0ycTOgjdY9ErwXCc8QyrQp82bdL0Xkm9yA== @@ -16516,23 +16526,23 @@ linkify-it@^3.0.1: uc.micro "^1.0.1" lint-staged@^12.2.0: - version "12.4.1" - resolved "https://registry.npmjs.org/lint-staged/-/lint-staged-12.4.1.tgz#63fa27bfc8a33515f6902f63f6670864f1fb233c" - integrity sha512-PTXgzpflrQ+pODQTG116QNB+Q6uUTDg5B5HqGvNhoQSGt8Qy+MA/6zSnR8n38+sxP5TapzeQGTvoKni0KRS8Vg== + version "12.4.3" + resolved "https://registry.npmjs.org/lint-staged/-/lint-staged-12.4.3.tgz#914fa468458364e14cc952145db552d87c8847b6" + integrity sha512-eH6SKOmdm/ZwCRMTZAmM3q3dPkpq6vco/BfrOw8iGun4Xs/thYegPD/MLIwKO+iPkzibkLJuQcRhRLXKvaKreg== dependencies: cli-truncate "^3.1.0" colorette "^2.0.16" - commander "^8.3.0" - debug "^4.3.3" + commander "^9.3.0" + debug "^4.3.4" execa "^5.1.1" - lilconfig "2.0.4" - listr2 "^4.0.1" - micromatch "^4.0.4" + lilconfig "2.0.5" + listr2 "^4.0.5" + micromatch "^4.0.5" normalize-path "^3.0.0" - object-inspect "^1.12.0" + object-inspect "^1.12.2" pidtree "^0.5.0" string-argv "^0.3.1" - supports-color "^9.2.1" + supports-color "^9.2.2" yaml "^1.10.2" listenercount@~1.0.1: @@ -16583,7 +16593,7 @@ listr2@^3.8.3: through "^2.3.8" wrap-ansi "^7.0.0" -listr2@^4.0.1: +listr2@^4.0.5: version "4.0.5" resolved "https://registry.npmjs.org/listr2/-/listr2-4.0.5.tgz#9dcc50221583e8b4c71c43f9c7dfd0ef546b75d5" integrity sha512-juGHV1doQdpNT3GSTs9IUN43QJb7KHdF9uqg7Vufs/tG9VTzpFphqF4pm/ICdAABGQxsyNn9CiYA3StkI6jpwA== @@ -17817,7 +17827,7 @@ micromatch@^3.1.10: snapdragon "^0.8.1" to-regex "^3.0.2" -micromatch@^4.0.2, micromatch@^4.0.4: +micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5: version "4.0.5" resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== @@ -18833,11 +18843,16 @@ object-hash@^2.0.1: resolved "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz#5ad518581eefc443bd763472b8ff2e9c2c0d54a5" integrity sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw== -object-inspect@^1.11.0, object-inspect@^1.12.0, object-inspect@^1.9.0: +object-inspect@^1.11.0, object-inspect@^1.9.0: version "1.12.0" resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz#6e2c120e868fd1fd18cb4f18c31741d0d6e776f0" integrity sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g== +object-inspect@^1.12.2: + version "1.12.2" + resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz#c0641f26394532f28ab8d796ab954e43c009a8ea" + integrity sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ== + object-keys@^1.0.12, object-keys@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" @@ -23550,7 +23565,7 @@ supports-color@^8.0.0, supports-color@^8.1.0, supports-color@^8.1.1: dependencies: has-flag "^4.0.0" -supports-color@^9.2.1: +supports-color@^9.2.2: version "9.2.2" resolved "https://registry.npmjs.org/supports-color/-/supports-color-9.2.2.tgz#502acaf82f2b7ee78eb7c83dcac0f89694e5a7bb" integrity sha512-XC6g/Kgux+rJXmwokjm9ECpD6k/smUoS5LKlUCcsYr4IY3rW0XyAympon2RmxGrlnZURMpg5T18gWDP9CsHXFA== From 3ac8c7723a0719c8c4c562be85e998367a168239 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 30 May 2022 15:22:06 +0000 Subject: [PATCH 13/22] fix(deps): update dependency @roadiehq/backstage-plugin-buildkite to v2.0.4 Signed-off-by: Renovate Bot --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 66125687b0..e67453105d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4908,9 +4908,9 @@ integrity sha512-8UiDeDbjCImFSfOegGu13otQ7OdP9FOYpcLjeouppnhs+MPeIEAtYS+jCcBKmi3reyTagC15/KVSRhde1wS1vg== "@roadiehq/backstage-plugin-buildkite@^2.0.0": - version "2.0.3" - resolved "https://registry.npmjs.org/@roadiehq/backstage-plugin-buildkite/-/backstage-plugin-buildkite-2.0.3.tgz#73f3586c176c4c2ffe55af78ee8ce86c3c363dc6" - integrity sha512-Jna1m/pj52G7qz0PKmldQUXHsw5IUTxtgxVh2n1X2CTq+o10xySVspmEyEmLdNxkeQdYO+a280fYWjEKo9xoyQ== + version "2.0.4" + resolved "https://registry.npmjs.org/@roadiehq/backstage-plugin-buildkite/-/backstage-plugin-buildkite-2.0.4.tgz#cd72fd35a9f8b6bb1c2ccd3c13d2aece2ecaf86e" + integrity sha512-bOrqKO9MmRB5jgue+S8WEmJk83h6g4EdGp2qmirVODQddL03gtFNgMTauQkKAV7T8N+C948xDcgdL3pjvLYJdA== dependencies: "@backstage/catalog-model" "^1.0.0" "@backstage/core-components" "^0.9.0" From aaeb0662521023728d9be0e27bf84e3ec71c339e Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 30 May 2022 16:12:18 +0000 Subject: [PATCH 14/22] fix(deps): update dependency @yarnpkg/parsers to v3.0.0-rc.6 Signed-off-by: Renovate Bot --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index e67453105d..3ee9833781 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7201,9 +7201,9 @@ integrity sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ== "@yarnpkg/parsers@^3.0.0-rc.4": - version "3.0.0-rc.4" - resolved "https://registry.npmjs.org/@yarnpkg/parsers/-/parsers-3.0.0-rc.4.tgz#d7b19fa22ce7ff2423e5cbf008f7be0a85e9f1e3" - integrity sha512-ScXXCUwGdx+aEIP20U8VEGtuXmxcMPFdJTb9G9a8MRpQPxIkky/GYXEL5Hf4oqJJXGyCv/DN31zfmxyj31SLKw== + version "3.0.0-rc.6" + resolved "https://registry.npmjs.org/@yarnpkg/parsers/-/parsers-3.0.0-rc.6.tgz#3c93267fdae4470e4eaaf8c5f81d0d00ea177f76" + integrity sha512-YqtJ9VQqQixZsJJS4X83e6RMpgK1jmQJSIrCfd1wO3i/7vPk9QoLvvZS4bwZ2ha8QWqWlO/alAcXCGBezEI1Ig== dependencies: js-yaml "^3.10.0" tslib "^1.13.0" From 863e14d466930144785ba1f6bb6b2e66477e3eda Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 30 May 2022 17:05:41 +0000 Subject: [PATCH 15/22] fix(deps): update dependency core-js to v3.22.7 Signed-off-by: Renovate Bot --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 3ee9833781..dd85b7ff0e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9885,9 +9885,9 @@ core-js@^2.4.0, core-js@^2.5.0, core-js@^2.6.10: integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ== core-js@^3.4.1, core-js@^3.6.5: - version "3.22.5" - resolved "https://registry.npmjs.org/core-js/-/core-js-3.22.5.tgz#a5f5a58e663d5c0ebb4e680cd7be37536fb2a9cf" - integrity sha512-VP/xYuvJ0MJWRAobcmQ8F2H6Bsn+s7zqAAjFaHGBMc5AQm7zaelhD1LGduFn2EehEcQcU+br6t+fwbpQ5d1ZWA== + version "3.22.7" + resolved "https://registry.npmjs.org/core-js/-/core-js-3.22.7.tgz#8d6c37f630f6139b8732d10f2c114c3f1d00024f" + integrity sha512-Jt8SReuDKVNZnZEzyEQT5eK6T2RRCXkfTq7Lo09kpm+fHjgGewSbNjV+Wt4yZMhPDdzz2x1ulI5z/w4nxpBseg== core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" From 566e26668347a8d13ccdfaafd31653561b546fbc Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Mon, 30 May 2022 19:19:28 +0200 Subject: [PATCH 16/22] Consume legacy context in hook too, just in case. Signed-off-by: Eric Peterson --- .../Sidebar/SidebarOpenStateContext.test.tsx | 20 +++++++++++++++++ .../Sidebar/SidebarOpenStateContext.tsx | 20 +++++++++-------- .../Sidebar/SidebarPinStateContext.test.tsx | 22 +++++++++++++++++++ .../layout/Sidebar/SidebarPinStateContext.tsx | 16 ++++++++------ 4 files changed, 62 insertions(+), 16 deletions(-) diff --git a/packages/core-components/src/layout/Sidebar/SidebarOpenStateContext.test.tsx b/packages/core-components/src/layout/Sidebar/SidebarOpenStateContext.test.tsx index 4953e1341b..f1900f025c 100644 --- a/packages/core-components/src/layout/Sidebar/SidebarOpenStateContext.test.tsx +++ b/packages/core-components/src/layout/Sidebar/SidebarOpenStateContext.test.tsx @@ -56,6 +56,26 @@ describe('SidebarOpenStateContext', () => { }); describe('useSidebarOpenState', () => { + it('can be invoked within legacy context', () => { + const wrapper = ({ children }: { children: ReactNode }) => ( + {}, + }} + > + {children} + + ); + + const { result } = renderHook(() => useSidebarOpenState(), { + wrapper, + }); + + expect(result.current.isOpen).toBe(true); + expect(typeof result.current.setOpen).toBe('function'); + }); + it('does not need to be invoked within provider', () => { const { result } = renderHook(() => useSidebarOpenState()); expect(result.current.isOpen).toBe(false); diff --git a/packages/core-components/src/layout/Sidebar/SidebarOpenStateContext.tsx b/packages/core-components/src/layout/Sidebar/SidebarOpenStateContext.tsx index be15785f29..635306959a 100644 --- a/packages/core-components/src/layout/Sidebar/SidebarOpenStateContext.tsx +++ b/packages/core-components/src/layout/Sidebar/SidebarOpenStateContext.tsx @@ -50,7 +50,7 @@ export type SidebarOpenState = { setOpen: (open: boolean) => void; }; -const defaultSidebarContext = { +const defaultSidebarOpenStateContext = { isOpen: false, setOpen: () => {}, }; @@ -62,7 +62,7 @@ const defaultSidebarContext = { * Use `` + `useSidebar()` instead. */ export const LegacySidebarContext = createContext( - defaultSidebarContext, + defaultSidebarOpenStateContext, ); const VersionedSidebarContext = createVersionedContext<{ @@ -97,17 +97,19 @@ export const SidebarOpenStateProvider = ({ * @public */ export const useSidebarOpenState = (): SidebarOpenState => { - const versionedSidebarContext = useContext(VersionedSidebarContext); + const versionedOpenStateContext = useContext(VersionedSidebarContext); + const legacyOpenStateContext = useContext(LegacySidebarContext); - // Invoked from outside a SidebarOpenStateProvider, return a default value. - if (versionedSidebarContext === undefined) { - return defaultSidebarContext; + // Invoked from outside a SidebarOpenStateProvider: check for the legacy + // context's value, but otherwise return the default. + if (versionedOpenStateContext === undefined) { + return legacyOpenStateContext || defaultSidebarOpenStateContext; } - const sidebarContext = versionedSidebarContext.atVersion(1); - if (sidebarContext === undefined) { + const openStateContext = versionedOpenStateContext.atVersion(1); + if (openStateContext === undefined) { throw new Error('No context found for version 1.'); } - return sidebarContext; + return openStateContext; }; diff --git a/packages/core-components/src/layout/Sidebar/SidebarPinStateContext.test.tsx b/packages/core-components/src/layout/Sidebar/SidebarPinStateContext.test.tsx index 1a86ec612e..8bd1597725 100644 --- a/packages/core-components/src/layout/Sidebar/SidebarPinStateContext.test.tsx +++ b/packages/core-components/src/layout/Sidebar/SidebarPinStateContext.test.tsx @@ -63,6 +63,28 @@ describe('SidebarPinStateContext', () => { }); describe('useSidebarPinState', () => { + it('can be invoked within legacy context', () => { + const wrapper = ({ children }: { children: ReactNode }) => ( + {}, + }} + > + {children} + + ); + + const { result } = renderHook(() => useSidebarPinState(), { + wrapper, + }); + + expect(result.current.isPinned).toBe(true); + expect(result.current.isMobile).toBe(true); + expect(typeof result.current.toggleSidebarPinState).toBe('function'); + }); + it('does not need to be invoked within provider', () => { const { result } = renderHook(() => useSidebarPinState()); expect(result.current.isPinned).toBe(true); diff --git a/packages/core-components/src/layout/Sidebar/SidebarPinStateContext.tsx b/packages/core-components/src/layout/Sidebar/SidebarPinStateContext.tsx index a85929ab70..e00ed259e9 100644 --- a/packages/core-components/src/layout/Sidebar/SidebarPinStateContext.tsx +++ b/packages/core-components/src/layout/Sidebar/SidebarPinStateContext.tsx @@ -103,17 +103,19 @@ export const SidebarPinStateProvider = ({ * @public */ export const useSidebarPinState = (): SidebarPinState => { - const versionedSidebarContext = useContext(VersionedSidebarPinStateContext); + const versionedPinStateContext = useContext(VersionedSidebarPinStateContext); + const legacyPinStateContext = useContext(LegacySidebarPinStateContext); - // Invoked from outside a SidebarPinStateProvider: default value. - if (versionedSidebarContext === undefined) { - return defaultSidebarPinStateContext; + // Invoked from outside a SidebarPinStateProvider: check for the legacy + // context's value, but otherwise return the default. + if (versionedPinStateContext === undefined) { + return legacyPinStateContext || defaultSidebarPinStateContext; } - const sidebarContext = versionedSidebarContext.atVersion(1); - if (sidebarContext === undefined) { + const pinStateContext = versionedPinStateContext.atVersion(1); + if (pinStateContext === undefined) { throw new Error('No context found for version 1.'); } - return sidebarContext; + return pinStateContext; }; From 3bb25a9accc730529cb7259d2e4439008b3209d3 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Fri, 29 Apr 2022 18:00:55 +0200 Subject: [PATCH 17/22] Initial implementation of a generic ndjson collator. Signed-off-by: Eric Peterson --- .changeset/search-eyes-of-them-all.md | 7 + .changeset/search-martha-sways.md | 5 + plugins/search-backend-node/api-report.md | 28 ++++ plugins/search-backend-node/package.json | 7 +- ...ewlineDelimitedJsonCollatorFactory.test.ts | 157 ++++++++++++++++++ .../NewlineDelimitedJsonCollatorFactory.ts | 147 ++++++++++++++++ .../src/collators/index.ts | 19 +++ plugins/search-backend-node/src/index.ts | 1 + .../src/test-utils/TestPipeline.ts | 8 +- yarn.lock | 19 +++ 10 files changed, 393 insertions(+), 5 deletions(-) create mode 100644 .changeset/search-eyes-of-them-all.md create mode 100644 .changeset/search-martha-sways.md create mode 100644 plugins/search-backend-node/src/collators/NewlineDelimitedJsonCollatorFactory.test.ts create mode 100644 plugins/search-backend-node/src/collators/NewlineDelimitedJsonCollatorFactory.ts create mode 100644 plugins/search-backend-node/src/collators/index.ts diff --git a/.changeset/search-eyes-of-them-all.md b/.changeset/search-eyes-of-them-all.md new file mode 100644 index 0000000000..4984d31877 --- /dev/null +++ b/.changeset/search-eyes-of-them-all.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-search-backend-node': patch +--- + +Introducing a `NewlineDelimitedJsonCollatorFactory`, which can be used to create search indices from newline delimited JSON files stored in external storage readable via a configured `UrlReader` instance. + +This is useful if you have an independent process periodically generating `*.ndjson` files consisting of `IndexableDocument` objects and want to be able to generate a fresh index based on the latest version of such a file. diff --git a/.changeset/search-martha-sways.md b/.changeset/search-martha-sways.md new file mode 100644 index 0000000000..10231a4205 --- /dev/null +++ b/.changeset/search-martha-sways.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search-backend-node': patch +--- + +Fixed a bug that prevented `TestPipeline.withSubject` from identifying valid `Readable` subjects that were technically transform streams. diff --git a/plugins/search-backend-node/api-report.md b/plugins/search-backend-node/api-report.md index 0f379b3fc1..8c10b031a2 100644 --- a/plugins/search-backend-node/api-report.md +++ b/plugins/search-backend-node/api-report.md @@ -5,6 +5,7 @@ ```ts /// +import { Config } from '@backstage/config'; import { DocumentCollatorFactory } from '@backstage/plugin-search-common'; import { DocumentDecoratorFactory } from '@backstage/plugin-search-common'; import { DocumentTypeInfo } from '@backstage/plugin-search-common'; @@ -12,6 +13,7 @@ import { IndexableDocument } from '@backstage/plugin-search-common'; import { IndexableResultSet } from '@backstage/plugin-search-common'; import { Logger } from 'winston'; import { default as lunr_2 } from 'lunr'; +import { Permission } from '@backstage/plugin-permission-common'; import { QueryTranslator } from '@backstage/plugin-search-common'; import { Readable } from 'stream'; import { SearchEngine } from '@backstage/plugin-search-common'; @@ -19,6 +21,7 @@ import { SearchQuery } from '@backstage/plugin-search-common'; import { TaskFunction } from '@backstage/backend-tasks'; import { TaskRunner } from '@backstage/backend-tasks'; import { Transform } from 'stream'; +import { UrlReader } from '@backstage/backend-common'; import { Writable } from 'stream'; // @beta @@ -112,6 +115,31 @@ export class LunrSearchEngineIndexer extends BatchSearchEngineIndexer { initialize(): Promise; } +// @beta +export class NewlineDelimitedJsonCollatorFactory + implements DocumentCollatorFactory +{ + static fromConfig( + _config: Config, + options: NewlineDelimitedJsonCollatorFactoryOptions, + ): NewlineDelimitedJsonCollatorFactory; + // (undocumented) + getCollator(): Promise; + // (undocumented) + readonly type: string; + // (undocumented) + readonly visibilityPermission: Permission | undefined; +} + +// @beta (undocumented) +export type NewlineDelimitedJsonCollatorFactoryOptions = { + type: string; + searchPattern: string; + reader: UrlReader; + logger: Logger; + visibilityPermission?: Permission; +}; + // @beta export interface RegisterCollatorParameters { factory: DocumentCollatorFactory; diff --git a/plugins/search-backend-node/package.json b/plugins/search-backend-node/package.json index 7a0b0631c6..759879aa5f 100644 --- a/plugins/search-backend-node/package.json +++ b/plugins/search-backend-node/package.json @@ -23,19 +23,24 @@ "clean": "backstage-cli package clean" }, "dependencies": { + "@backstage/backend-common": "^0.13.6-next.0", "@backstage/backend-tasks": "^0.3.2-next.0", + "@backstage/config": "^1.0.1", "@backstage/errors": "^1.0.0", + "@backstage/plugin-permission-common": "^0.6.1", "@backstage/plugin-search-common": "^0.3.4", "@types/lunr": "^2.3.3", "lodash": "^4.17.21", "lunr": "^2.3.9", + "ndjson": "^2.0.0", "node-abort-controller": "^3.0.1", "uuid": "^8.3.2", "winston": "^3.2.1" }, "devDependencies": { "@backstage/backend-common": "^0.13.6-next.0", - "@backstage/cli": "^0.17.2-next.0" + "@backstage/cli": "^0.17.2-next.0", + "@types/ndjson": "^2.0.1" }, "files": [ "dist" diff --git a/plugins/search-backend-node/src/collators/NewlineDelimitedJsonCollatorFactory.test.ts b/plugins/search-backend-node/src/collators/NewlineDelimitedJsonCollatorFactory.test.ts new file mode 100644 index 0000000000..198995b3cf --- /dev/null +++ b/plugins/search-backend-node/src/collators/NewlineDelimitedJsonCollatorFactory.test.ts @@ -0,0 +1,157 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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 { + getVoidLogger, + ReadUrlResponse, + UrlReader, + UrlReaders, +} from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; +import { Readable } from 'stream'; +import { NewlineDelimitedJsonCollatorFactory } from './NewlineDelimitedJsonCollatorFactory'; +import { TestPipeline } from '../test-utils'; + +describe('DefaultCatalogCollatorFactory', () => { + const config = new ConfigReader({}); + const logger = getVoidLogger(); + + it('has expected type', () => { + const factory = NewlineDelimitedJsonCollatorFactory.fromConfig(config, { + type: 'expected-type', + searchPattern: 'test://folder/prefix-*', + logger, + reader: UrlReaders.default({ logger, config }), + }); + expect(factory.type).toBe('expected-type'); + }); + + describe('getCollator', () => { + let readable: Readable; + let reader: jest.Mocked< + UrlReader & { readUrl: jest.Mock> } + >; + let factory: NewlineDelimitedJsonCollatorFactory; + + beforeEach(async () => { + jest.clearAllMocks(); + + readable = new Readable(); + readable._read = () => {}; + reader = { + search: jest.fn(), + read: jest.fn(), + readTree: jest.fn(), + readUrl: jest.fn(), + }; + factory = NewlineDelimitedJsonCollatorFactory.fromConfig(config, { + type: 'expected-type', + searchPattern: 'test://folder/prefix-*', + logger, + reader: UrlReaders.create({ + logger, + config, + factories: [() => [{ predicate: () => true, reader }]], + }), + }); + }); + + it('throws if url reader throws an error during search', async () => { + reader.search.mockRejectedValue(new Error('Expected error')); + + await expect(() => factory.getCollator()).rejects.toThrowError( + 'Expected error', + ); + }); + + it('throws if no matching files are found', async () => { + reader.search.mockResolvedValue({ files: [], etag: '' }); + + await expect(() => factory.getCollator()).rejects.toThrowError( + 'Could not find an .ndjson file matching', + ); + }); + + it('throws if matching file is not .ndjson', async () => { + reader.search.mockResolvedValue({ + files: [{ url: 'test://folder/prefix-1.avro', content: jest.fn() }], + etag: '', + }); + reader.readUrl.mockResolvedValue({ + buffer: jest.fn(), + stream: jest.fn().mockReturnValue(readable), + }); + + await expect(() => factory.getCollator()).rejects.toThrowError( + 'Could not find an .ndjson file matching', + ); + }); + + it('gets stream using latest matched url', async () => { + reader.search.mockResolvedValue({ + files: [ + { url: 'test://folder/prefix-1.ndjson', content: jest.fn() }, + { url: 'test://folder/prefix-2.ndjson', content: jest.fn() }, + ], + etag: '', + }); + reader.readUrl.mockResolvedValue({ + buffer: jest.fn(), + stream: jest.fn().mockReturnValue(readable), + }); + + await factory.getCollator(); + + expect(reader.search).toHaveBeenCalledWith( + 'test://folder/prefix-*', + undefined, + ); + expect(reader.readUrl).toHaveBeenCalledWith( + 'test://folder/prefix-2.ndjson', + undefined, + ); + }); + + it('transforms newline delimited json into readable stream of documents', async () => { + reader.search.mockResolvedValue({ + files: [{ url: 'test://folder/prefix-1.ndjson', content: jest.fn() }], + etag: '', + }); + reader.readUrl.mockResolvedValue({ + buffer: jest.fn(), + stream: jest + .fn() + .mockReturnValue( + Readable.from( + '{"title": "Title 1", "location": "/title-1", "text": "text 1"}\n{"title": "Title 2", "location": "/title-2", "text": "text 2"}', + ), + ), + }); + + const collator = await factory.getCollator(); + const pipeline = TestPipeline.withSubject(collator); + const { documents } = await pipeline.execute(); + + expect(documents).toHaveLength(2); + expect(documents[0].title).toBe('Title 1'); + expect(documents[0].location).toBe('/title-1'); + expect(documents[0].text).toBe('text 1'); + expect(documents[1].title).toBe('Title 2'); + expect(documents[1].location).toBe('/title-2'); + expect(documents[1].text).toBe('text 2'); + }); + }); +}); diff --git a/plugins/search-backend-node/src/collators/NewlineDelimitedJsonCollatorFactory.ts b/plugins/search-backend-node/src/collators/NewlineDelimitedJsonCollatorFactory.ts new file mode 100644 index 0000000000..42c43818f1 --- /dev/null +++ b/plugins/search-backend-node/src/collators/NewlineDelimitedJsonCollatorFactory.ts @@ -0,0 +1,147 @@ +/* + * Copyright 2021 The Backstage Authors + * + * 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 { UrlReader } from '@backstage/backend-common'; +import { Config } from '@backstage/config'; +import { Permission } from '@backstage/plugin-permission-common'; +import { DocumentCollatorFactory } from '@backstage/plugin-search-common'; +import { parse as parseNdjson } from 'ndjson'; +import { Readable } from 'stream'; +import { Logger } from 'winston'; + +/** + * @beta + */ +export type NewlineDelimitedJsonCollatorFactoryOptions = { + type: string; + searchPattern: string; + reader: UrlReader; + logger: Logger; + visibilityPermission?: Permission; +}; + +/** + * Factory class producing a collator that can be used to index documents + * sourced from the latest newline delimited JSON file matching a given search + * pattern. "Latest" is determined by the name of the file (last alphabetically + * is considered latest). + * + * @remarks + * The reader provided must implement the `search()` method as well as the + * `readUrl` method whose response includes the `stream()` method. Naturally, + * the reader must also be configured to understand the given search pattern. + * + * @example + * Here's an example configuration using Google Cloud Storage, which would + * return the latest file under the `bucket` GCS bucket with files like + * `xyz-2021.ndjson` or `xyz-2022.ndjson`. + * ```ts + * indexBuilder.addCollator({ + * schedule, + * factory: NewlineDelimitedJsonCollatorFactory.fromConfig(env.config, { + * type: 'techdocs', + * searchPattern: 'https://storage.cloud.google.com/bucket/xyz-*', + * reader: env.reader, + * logger: env.logger, + * }) + * }); + * ``` + * + * @beta + */ +export class NewlineDelimitedJsonCollatorFactory + implements DocumentCollatorFactory +{ + /** {@inheritDoc @backstage/plugin-search-common#DocumentCollatorFactory."type"} */ + readonly type: string; + + /** {@inheritDoc @backstage/plugin-search-common#DocumentCollatorFactory.visibilityPermission} */ + public readonly visibilityPermission: Permission | undefined; + + private constructor( + type: string, + private readonly searchPattern: string, + private readonly reader: UrlReader, + private readonly logger: Logger, + visibilityPermission: Permission | undefined, + ) { + this.type = type; + this.visibilityPermission = visibilityPermission; + } + + /** + * Returns a NewlineDelimitedJsonCollatorFactory instance from configuration + * and a set of options. + */ + static fromConfig( + _config: Config, + options: NewlineDelimitedJsonCollatorFactoryOptions, + ): NewlineDelimitedJsonCollatorFactory { + return new NewlineDelimitedJsonCollatorFactory( + options.type, + options.searchPattern, + options.reader, + options.logger, + options.visibilityPermission, + ); + } + + /** + * Returns the "latest" URL for the given search pattern (e.g. the one at the + * end of the list, sorted alphabetically). + */ + private async lastUrl(): Promise { + try { + // Search for files matching the given pattern, then sort/reverse. The + // first item in the list will be the "latest" file. + this.logger.info( + `Attempting to find latest .ndjson matching ${this.searchPattern}`, + ); + const { files } = await this.reader.search(this.searchPattern); + const candidates = files + .filter(file => file.url.endsWith('.ndjson')) + .sort((a, b) => a.url.localeCompare(b.url)) + .reverse(); + + return candidates[0]?.url; + } catch (e) { + this.logger.error(`Could not search for ${this.searchPattern}`, e); + throw e; + } + } + + /** {@inheritDoc @backstage/plugin-search-common#DocumentCollatorFactory.getCollator} */ + async getCollator(): Promise { + // Search for files matching the given pattern. + const lastUrl = await this.lastUrl(); + + // Abort if no such file could be found. + if (!lastUrl) { + const noMatchingFile = `Could not find an .ndjson file matching ${this.searchPattern}`; + this.logger.error(noMatchingFile); + throw new Error(noMatchingFile); + } else { + this.logger.info(`Using latest .ndjson file ${lastUrl}`); + } + + // Use the UrlReader to try and stream the file. + const readerResponse = await this.reader.readUrl!(lastUrl); + const stream = readerResponse.stream!(); + + // Use ndjson's parser to turn the raw file into an object-mode stream. + return stream.pipe(parseNdjson()); + } +} diff --git a/plugins/search-backend-node/src/collators/index.ts b/plugins/search-backend-node/src/collators/index.ts new file mode 100644 index 0000000000..82598d2727 --- /dev/null +++ b/plugins/search-backend-node/src/collators/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2021 The Backstage Authors + * + * 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 { NewlineDelimitedJsonCollatorFactoryOptions } from './NewlineDelimitedJsonCollatorFactory'; + +export { NewlineDelimitedJsonCollatorFactory } from './NewlineDelimitedJsonCollatorFactory'; diff --git a/plugins/search-backend-node/src/index.ts b/plugins/search-backend-node/src/index.ts index e5202ac640..d342bb9259 100644 --- a/plugins/search-backend-node/src/index.ts +++ b/plugins/search-backend-node/src/index.ts @@ -22,6 +22,7 @@ export { IndexBuilder } from './IndexBuilder'; export { Scheduler } from './Scheduler'; +export * from './collators'; export { LunrSearchEngine } from './engines'; export type { ConcreteLunrQuery, diff --git a/plugins/search-backend-node/src/test-utils/TestPipeline.ts b/plugins/search-backend-node/src/test-utils/TestPipeline.ts index dd90de4f38..688cab55be 100644 --- a/plugins/search-backend-node/src/test-utils/TestPipeline.ts +++ b/plugins/search-backend-node/src/test-utils/TestPipeline.ts @@ -66,14 +66,14 @@ export class TestPipeline { return new TestPipeline({ decorator: subject }); } - if (subject instanceof Readable) { - return new TestPipeline({ collator: subject }); - } - if (subject instanceof Writable) { return new TestPipeline({ indexer: subject }); } + if (subject.readable || subject instanceof Readable) { + return new TestPipeline({ collator: subject }); + } + throw new Error( 'Unknown test subject: are you passing a readable, writable, or transform stream?', ); diff --git a/yarn.lock b/yarn.lock index 3ee9833781..1df2bf6928 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6224,6 +6224,14 @@ resolved "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197" integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA== +"@types/ndjson@^2.0.1": + version "2.0.1" + resolved "https://registry.npmjs.org/@types/ndjson/-/ndjson-2.0.1.tgz#0279bc20949bfb861d69ac3de5292775b169a2d0" + integrity sha512-xSRLa/CtPjEo0plSQj+nMKjVBkYh5MeMwOXa1y//jFELdmy9AmVQgWKWQgZ+/XrNlAYxXtmKR8OHaizPgEpUEw== + dependencies: + "@types/node" "*" + "@types/through" "*" + "@types/node-fetch@^2.5.0", "@types/node-fetch@^2.5.12", "@types/node-fetch@^2.5.7": version "2.6.1" resolved "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.1.tgz#8f127c50481db65886800ef496f20bbf15518975" @@ -18367,6 +18375,17 @@ natural-compare@^1.4.0: resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= +ndjson@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/ndjson/-/ndjson-2.0.0.tgz#320ac86f6fe53f5681897349b86ac6f43bfa3a19" + integrity sha512-nGl7LRGrzugTtaFcJMhLbpzJM6XdivmbkdlaGcrk/LXg2KL/YBC6z1g70xh0/al+oFuVFP8N8kiWRucmeEH/qQ== + dependencies: + json-stringify-safe "^5.0.1" + minimist "^1.2.5" + readable-stream "^3.6.0" + split2 "^3.0.0" + through2 "^4.0.0" + negotiator@0.6.3, negotiator@^0.6.2, negotiator@^0.6.3: version "0.6.3" resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" From eb6a4d40af15cba64b4853fb2fcff48d203c49be Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Fri, 27 May 2022 13:30:04 +0200 Subject: [PATCH 18/22] Move from under Search to under Plugins Signed-off-by: Eric Peterson --- .../integrating-search-into-plugins.md} | 6 +++--- microsite/sidebars.json | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) rename docs/{features/search/search-for-plugin-devs.md => plugins/integrating-search-into-plugins.md} (98%) diff --git a/docs/features/search/search-for-plugin-devs.md b/docs/plugins/integrating-search-into-plugins.md similarity index 98% rename from docs/features/search/search-for-plugin-devs.md rename to docs/plugins/integrating-search-into-plugins.md index 70697cd91c..77456dd5b4 100644 --- a/docs/features/search/search-for-plugin-devs.md +++ b/docs/plugins/integrating-search-into-plugins.md @@ -1,7 +1,7 @@ --- -id: for-plugin-devs -title: Search for Plugin Developers -description: How to integrate search into a Backstage plugin +id: integrating-search-into-plugins +title: Integrating Search into a plugin +description: How to integrate Search into a Backstage plugin --- The Backstage Search Platform was designed to give plugin developers the APIs diff --git a/microsite/sidebars.json b/microsite/sidebars.json index e85eba8ba1..d570205dd5 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -99,8 +99,7 @@ "features/search/concepts", "features/search/architecture", "features/search/search-engines", - "features/search/how-to-guides", - "features/search/for-plugin-devs" + "features/search/how-to-guides" ] }, { @@ -199,6 +198,7 @@ "plugins/plugin-development", "plugins/structure-of-a-plugin", "plugins/integrating-plugin-into-software-catalog", + "plugins/integrating-search-into-plugins", "plugins/composability", "plugins/analytics", { From fd16617e28d9d090b96d5e0fcf0b015935a6aa3c Mon Sep 17 00:00:00 2001 From: Leon Date: Mon, 30 May 2022 19:58:40 +0200 Subject: [PATCH 19/22] fix api-report warning --- plugins/tech-insights/api-report.md | 2 -- plugins/tech-insights/src/api/TechInsightsClient.ts | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/tech-insights/api-report.md b/plugins/tech-insights/api-report.md index 41e37ca4ff..54884f12ea 100644 --- a/plugins/tech-insights/api-report.md +++ b/plugins/tech-insights/api-report.md @@ -80,8 +80,6 @@ export interface TechInsightsApi { // @public export const techInsightsApiRef: ApiRef; -// Warning: (ae-missing-release-tag) "TechInsightsClient" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export class TechInsightsClient implements TechInsightsApi { constructor(options: { diff --git a/plugins/tech-insights/src/api/TechInsightsClient.ts b/plugins/tech-insights/src/api/TechInsightsClient.ts index 899e000695..aabb5a6297 100644 --- a/plugins/tech-insights/src/api/TechInsightsClient.ts +++ b/plugins/tech-insights/src/api/TechInsightsClient.ts @@ -29,6 +29,7 @@ import { defaultCheckResultRenderers, } from '../components/CheckResultRenderer'; +/** @public */ export class TechInsightsClient implements TechInsightsApi { private readonly discoveryApi: DiscoveryApi; private readonly identityApi: IdentityApi; From 09f4c2c2ab12d3a577c5d43fc5207cc053f491a8 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 30 May 2022 18:02:11 +0000 Subject: [PATCH 20/22] fix(deps): update dependency humanize-duration to v3.27.2 Signed-off-by: Renovate Bot --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index dd85b7ff0e..3b4ccd4b1a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14080,9 +14080,9 @@ human-signals@^2.1.0: integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== humanize-duration@^3.25.1, humanize-duration@^3.26.0, humanize-duration@^3.27.0, humanize-duration@^3.27.1: - version "3.27.1" - resolved "https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.27.1.tgz#2cd4ea4b03bd92184aee6d90d77a8f3d7628df69" - integrity sha512-jCVkMl+EaM80rrMrAPl96SGG4NRac53UyI1o/yAzebDntEY6K6/Fj2HOjdPg8omTqIe5Y0wPBai2q5xXrIbarA== + version "3.27.2" + resolved "https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.27.2.tgz#4b4e565bec098d22c9a54344e16156d1c649f160" + integrity sha512-A15OmA3FLFRnehvF4ZMocsxTZYvHq4ze7L+AgR1DeHw0xC9vMd4euInY83uqGU9/XXKNnVIEeKc1R8G8nKqtzg== humanize-ms@^1.2.1: version "1.2.1" From b0e42e645bf07b3d9fba21c286cd34124fa6b214 Mon Sep 17 00:00:00 2001 From: Chris Simmons Date: Tue, 31 May 2022 11:56:27 +1200 Subject: [PATCH 21/22] add laybuy to adopters list Signed-off-by: Chris Simmons --- ADOPTERS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/ADOPTERS.md b/ADOPTERS.md index d851686e0c..94bb3beb92 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -132,3 +132,4 @@ _If you're using Backstage in your organization, please try to add your company |[The Warehouse Group](https://www.thewarehouse.co.nz)|[Matt Law](mailto:matt.law@thewarehouse.co.nz)|Backstage enables us to bootstrap our middleware environment of new services for our Dev teams in a matter of seconds. CI, CD, testing, logging, deployments are all taken care of to get them up and running in less than 60 seconds. | [Tink](https://tink.com/) | [Sebastian Olsson](https://github.com/Sebelino), [Błażej Szum](https://github.com/blazejszumtink), [Anders Eurenius Runvald](https://github.com/anders-er-at-tink) | Internal developer portal which provides templates for creating new Java or Go microservices seamlessly. Also includes a tech radar and a visualization of our CD pipeline. | | [Brandwatch](https://brandwatch.com)| [Stefan Buck](https://github.com/stefanbuck) | Our primary focus is on the service catalog. Backstage is replacing our homemade service catalog. The switch was quite simple due to the catalog processor API. +| [Laybuy](https://www.laybuy.com)| [Chris Simmons](https://github.com/contrarianchris) | Backstage is the heart of Laybuy’s new centralised Development Platform, bringing disparate development tools and experiences into a single easy-to-use portal. It simplifies software and API discovery, project scaffolding, and technical documentation, enabling us to embrace golden path development and automate software standards. From 1fe9d5747cea16f58fdff9d271528ac0f91f434b Mon Sep 17 00:00:00 2001 From: Jakub Cierlik Date: Tue, 31 May 2022 08:48:01 +0200 Subject: [PATCH 22/22] Cloudify Plugin metadata updates Signed-off-by: Jakub Cierlik --- microsite/data/plugins/cloudify.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/microsite/data/plugins/cloudify.yaml b/microsite/data/plugins/cloudify.yaml index 394e1fea24..717f54737f 100644 --- a/microsite/data/plugins/cloudify.yaml +++ b/microsite/data/plugins/cloudify.yaml @@ -3,7 +3,7 @@ title: Cloudify author: Cloudify authorUrl: https://cloudify.co/ category: Orchestration -description: Load blueprints from desired Cloudify Manager instance -documentation: https://github.com/Cloudify-PS/backstage-cloudify-plugin#readme +description: Cloudify provides a remote execution and environment management backend that handles the provisioning and continuous update of Kubernetes, Terraform, Ansible, CloudFormation, Azure ARM, VRO based environments through a single API endpoint. +documentation: https://github.com/cloudify-cosmo/backstage-cloudify-plugin#readme iconUrl: https://avatars.githubusercontent.com/u/6260555?s=200&v=4 npmPackageName: 'plugin-cloudify'