diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index ec03e0e5dd..d792f2ed44 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -89,7 +89,7 @@ import { searchPage } from './components/search/SearchPage'; import { providers } from './identityProviders'; import * as plugins from './plugins'; -// import { techDocsPage } from './components/techdocs/TechDocsPage'; +import { techDocsPage } from './components/techdocs/TechDocsPage'; import { ApacheAirflowPage } from '@backstage/plugin-apache-airflow'; import { PermissionedRoute } from '@backstage/plugin-permission-react'; import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common'; @@ -184,7 +184,7 @@ const routes = ( } /> } + element={{techDocsPage}} > diff --git a/packages/app/src/components/techdocs/TechDocsPage.tsx b/packages/app/src/components/techdocs/TechDocsPage.tsx index e5142472f0..170d4f8f77 100644 --- a/packages/app/src/components/techdocs/TechDocsPage.tsx +++ b/packages/app/src/components/techdocs/TechDocsPage.tsx @@ -17,16 +17,15 @@ import { TechDocsReaderPageHeader, TechDocsReaderPageContent, - TechDocsReaderPage, } from '@backstage/plugin-techdocs'; import React from 'react'; const DefaultTechDocsPage = () => { return ( - + <> - + ); }; diff --git a/packages/techdocs-addons/src/types.ts b/packages/techdocs-addons/src/types.ts index 2eb8a2f57e..8e42e0a6bc 100644 --- a/packages/techdocs-addons/src/types.ts +++ b/packages/techdocs-addons/src/types.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import { Entity } from '@backstage/catalog-model'; import { ComponentType } from 'react'; import { AsyncState } from 'react-use/lib/useAsyncFn'; diff --git a/packages/techdocs-cli-embedded-app/src/components/TechDocsPage/TechDocsPage.tsx b/packages/techdocs-cli-embedded-app/src/components/TechDocsPage/TechDocsPage.tsx index 56686dd081..7bfe7642ff 100644 --- a/packages/techdocs-cli-embedded-app/src/components/TechDocsPage/TechDocsPage.tsx +++ b/packages/techdocs-cli-embedded-app/src/components/TechDocsPage/TechDocsPage.tsx @@ -29,14 +29,11 @@ import LightIcon from '@material-ui/icons/Brightness7'; import DarkIcon from '@material-ui/icons/Brightness4'; import { lightTheme, darkTheme } from '@backstage/theme'; -import { CompoundEntityRef } from '@backstage/catalog-model'; - -import { Content } from '@backstage/core-components'; import { - Reader, TechDocsReaderPage, TechDocsReaderPageHeader, + TechDocsReaderPageContent, } from '@backstage/plugin-techdocs'; const useStyles = makeStyles((theme: Theme) => ({ @@ -123,47 +120,13 @@ const TechDocsThemeToggle = () => { ); }; -const TechDocsPageContent = ({ - onReady, - entityRef, -}: { - entityRef: CompoundEntityRef; - onReady: () => void; -}) => { - const classes = useStyles(); - - return ( - - - - ); -}; - -const DefaultTechDocsPage = () => { - const techDocsMetadata = { - site_name: 'Live preview environment', - site_description: '', - }; - - return ( - - {({ entityRef, onReady }) => ( - <> - - - - - - )} - - ); -}; - export const techDocsPage = ( - + + + + + + ); diff --git a/plugins/techdocs/dev/index.tsx b/plugins/techdocs/dev/index.tsx index d08e914736..aa4b7bde6c 100644 --- a/plugins/techdocs/dev/index.tsx +++ b/plugins/techdocs/dev/index.tsx @@ -19,7 +19,8 @@ import { NotFoundError } from '@backstage/errors'; import React from 'react'; import { CompoundEntityRef } from '@backstage/catalog-model'; import { - Reader, + TechDocsReaderPageProvider, + TechDocsReaderPageContent, SyncResult, TechDocsStorageApi, techdocsStorageApiRef, @@ -112,13 +113,15 @@ function createPage({ render() { return ( - + > + + ); } } diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.tsx index f49f7b74c1..0ba0b47458 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.tsx @@ -69,7 +69,7 @@ export const TechDocsReaderPage = ({ if (defaultPath) { return defaultPath; } - return params['*'] ?? ''; + return params['*']; }, [params, defaultPath]); const entityName = useMemo(() => { diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPage/context.test.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPage/context.test.tsx index 921c2efd7d..f27e5aeb58 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPage/context.test.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPage/context.test.tsx @@ -15,7 +15,17 @@ */ import React from 'react'; -import { TechDocsMetadata } from './types'; +import { renderHook, act } from '@testing-library/react-hooks'; + +import { ThemeProvider } from '@material-ui/core'; + +import { lightTheme } from '@backstage/theme'; +import { TestApiProvider } from '@backstage/test-utils'; +import { Entity, CompoundEntityRef } from '@backstage/catalog-model'; + +import { techdocsApiRef } from '../../../api'; +import { TechDocsMetadata } from '../../../types'; + import { useEntityMetadata, useTechDocsMetadata, @@ -24,14 +34,17 @@ import { TechDocsMetadataProvider, TechDocsReaderPageProvider, } from './context'; -import { renderHook, act } from '@testing-library/react-hooks'; -import { Entity, CompoundEntityRef } from '@backstage/catalog-model'; - -const mockEntity: Entity = { +const mockEntityMetadata: Entity = { apiVersion: 'v1', kind: 'Component', - metadata: { name: 'test-component', namespace: 'default' }, + metadata: { + name: 'test', + namespace: 'default', + }, + spec: { + owner: 'test', + }, }; const mockTechDocsMetadata: TechDocsMetadata = { @@ -46,39 +59,42 @@ const mockShadowRoot = () => { return shadowRoot; }; +const techdocsApiMock = { + getEntityMetadata: jest.fn().mockResolvedValue(mockEntityMetadata), + getTechDocsMetadata: jest.fn().mockResolvedValue(mockTechDocsMetadata), +}; + const wrapper = ({ + path = '', entityName = { - namespace: mockEntity.metadata.namespace!!, - kind: mockEntity.kind, - name: mockEntity.metadata.name, + kind: mockEntityMetadata.kind, + name: mockEntityMetadata.metadata.name, + namespace: mockEntityMetadata.metadata.namespace!!, }, children, }: { - entityName: CompoundEntityRef; + path?: string; + entityName?: CompoundEntityRef; children: React.ReactNode; }) => ( - - - - {children} - - - + + + + + + {children} + + + + + ); describe('context', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + describe('useEntityMetadata', () => { it('should return loading state', async () => { const { result } = renderHook(() => useEntityMetadata()); @@ -87,11 +103,16 @@ describe('context', () => { }); it('should return expected entity values', async () => { - const { result } = renderHook(() => useEntityMetadata(), { wrapper }); + const { result, waitForNextUpdate } = renderHook( + () => useEntityMetadata(), + { wrapper }, + ); + + await waitForNextUpdate(); expect(result.current.value).toBeDefined(); expect(result.current.error).toBeUndefined(); - expect(result.current.value).toMatchObject(mockEntity); + expect(result.current.value).toMatchObject(mockEntityMetadata); }); }); @@ -103,7 +124,12 @@ describe('context', () => { }); it('should return expected techdocs metadata values', async () => { - const { result } = renderHook(() => useTechDocsMetadata(), { wrapper }); + const { result, waitForNextUpdate } = renderHook( + () => useTechDocsMetadata(), + { wrapper }, + ); + + await waitForNextUpdate(); expect(result.current.value).toBeDefined(); expect(result.current.error).toBeUndefined(); @@ -112,32 +138,49 @@ describe('context', () => { }); describe('useTechDocsReaderPage', () => { - it('should set title', () => { - const { result } = renderHook(() => useTechDocsReaderPage(), { wrapper }); + it('should set title', async () => { + const { result, waitForNextUpdate } = renderHook( + () => useTechDocsReaderPage(), + { wrapper }, + ); expect(result.current.title).toBe(''); act(() => result.current.setTitle('test site title')); + + await waitForNextUpdate(); + expect(result.current.title).toBe('test site title'); }); - it('should set subtitle', () => { - const { result } = renderHook(() => useTechDocsReaderPage(), { wrapper }); + it('should set subtitle', async () => { + const { result, waitForNextUpdate } = renderHook( + () => useTechDocsReaderPage(), + { wrapper }, + ); expect(result.current.subtitle).toBe(''); act(() => result.current.setSubtitle('test site subtitle')); + + await waitForNextUpdate(); + expect(result.current.subtitle).toBe('test site subtitle'); }); it('should set shadow root', async () => { - const { result } = renderHook(() => useTechDocsReaderPage(), { wrapper }); + const { result, waitForNextUpdate } = renderHook( + () => useTechDocsReaderPage(), + { wrapper }, + ); // mock shadowroot const shadowRoot = mockShadowRoot(); act(() => result.current.setShadowRoot(shadowRoot)); + await waitForNextUpdate(); + expect(result.current.shadowRoot?.innerHTML).toBe( '

Shadow DOM Mock

', ); diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPage/context.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPage/context.tsx index 0c580d17cd..56e0991df6 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPage/context.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPage/context.tsx @@ -31,9 +31,8 @@ import { CompoundEntityRef } from '@backstage/catalog-model'; import { techdocsApiRef } from '../../../api'; import { TechDocsEntityMetadata, TechDocsMetadata } from '../../../types'; -type PropsWithEntityName = PropsWithChildren< - T & { entityName: CompoundEntityRef } ->; +type PropsWithEntityName = T & + PropsWithChildren<{ entityName: CompoundEntityRef }>; const initialContextValue = { loading: true, @@ -129,16 +128,16 @@ export const useTechDocsReaderPage = () => { }; type TechDocsReaderPageProviderProps = PropsWithEntityName<{ - path: string; + path?: string; }>; export const TechDocsReaderPageProvider = ({ - path, + path = '', entityName, children, }: TechDocsReaderPageProviderProps) => { - const metadata = useTechDocsMetadata(); - const entityMetadata = useEntityMetadata(); + const { value: entityMetadataValue } = useEntityMetadata(); + const { value: techdocsMetadataValue } = useTechDocsMetadata(); const [title, setTitle] = useState(defaultTechDocsReaderPageValue.title); const [subtitle, setSubtitle] = useState( @@ -165,8 +164,8 @@ export const TechDocsReaderPageProvider = ({ {children instanceof Function ? children({ entityRef: entityName, - techdocsMetadataValue: metadata.value, - entityMetadataValue: entityMetadata.value, + entityMetadataValue, + techdocsMetadataValue, }) : children} diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/context.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/context.tsx index 007b1fcc65..7a4877d3cd 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/context.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/context.tsx @@ -25,7 +25,8 @@ import React, { } from 'react'; import { useNavigate } from 'react-router-dom'; -import { useTheme, Theme, lighten, alpha } from '@material-ui/core'; +import { useTheme, Theme } from '@material-ui/core'; +import { lighten, alpha } from '@material-ui/core/styles'; import { BackstageTheme } from '@backstage/theme'; import { CompoundEntityRef } from '@backstage/catalog-model'; diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPageHeader/TechDocsReaderPageHeader.test.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPageHeader/TechDocsReaderPageHeader.test.tsx index 9245bb66c5..3fb05a400e 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPageHeader/TechDocsReaderPageHeader.test.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPageHeader/TechDocsReaderPageHeader.test.tsx @@ -14,41 +14,91 @@ * limitations under the License. */ import React from 'react'; -import { TechDocsReaderPageHeader } from './TechDocsReaderPageHeader'; -import { act } from '@testing-library/react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { act, waitFor } from '@testing-library/react'; + +import { ThemeProvider } from '@material-ui/core'; + +import { lightTheme } from '@backstage/theme'; +import { CompoundEntityRef } from '@backstage/catalog-model'; import { entityRouteRef } from '@backstage/plugin-catalog-react'; -import { rootRouteRef } from '../../routes'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; + +import { techdocsApiRef } from '../../../api'; +import { rootRouteRef } from '../../../routes'; + +import { + TechDocsEntityProvider, + TechDocsMetadataProvider, + TechDocsReaderPageProvider, +} from '../TechDocsReaderPage'; + +import { TechDocsReaderPageHeader } from './TechDocsReaderPageHeader'; + +const mockEntityMetadata = { + locationMetadata: { + type: 'github', + target: 'https://example.com/', + }, + apiVersion: 'v1', + kind: 'test', + metadata: { + name: 'test-name', + namespace: 'test-namespace', + }, + spec: { + owner: 'test', + }, +}; + +const mockTechDocsMetadata = { + site_name: 'test-site-name', + site_description: 'test-site-desc', +}; + +const getEntityMetadata = jest.fn(); +const getTechDocsMetadata = jest.fn(); + +const techdocsApiMock = { + getEntityMetadata, + getTechDocsMetadata, +}; + +const Wrapper = ({ + path = '', + entityName = { + kind: mockEntityMetadata.kind, + name: mockEntityMetadata.metadata.name, + namespace: mockEntityMetadata.metadata.namespace!!, + }, + children, +}: { + path?: string; + entityName?: CompoundEntityRef; + children: React.ReactNode; +}) => ( + + + + + + {children} + + + + + +); describe('', () => { it('should render a techdocs page header', async () => { + getEntityMetadata.mockResolvedValue(mockEntityMetadata); + getTechDocsMetadata.mockResolvedValue(mockTechDocsMetadata); + await act(async () => { const rendered = await renderInTestApp( - , + + + , { mountedRoutes: { '/catalog/:namespace/:kind/:name/*': entityRouteRef, @@ -58,7 +108,11 @@ describe('', () => { ); expect(rendered.container.innerHTML).toContain('header'); - expect(rendered.getAllByText('test-site-name')).toHaveLength(2); + + await waitFor(() => { + expect(rendered.getAllByText('test-site-name')).toHaveLength(2); + }); + expect(rendered.getByText('test-site-desc')).toBeDefined(); }); }); @@ -66,13 +120,9 @@ describe('', () => { it('should render a techdocs page header even if metadata is missing', async () => { await act(async () => { const rendered = await renderInTestApp( - , + + + , { mountedRoutes: { '/catalog/:namespace/:kind/:name/*': entityRouteRef, @@ -86,19 +136,13 @@ describe('', () => { }); it('should render a link back to the component page', async () => { + getTechDocsMetadata.mockResolvedValue(mockTechDocsMetadata); + await act(async () => { const rendered = await renderInTestApp( - , + + + , { mountedRoutes: { '/catalog/:namespace/:kind/:name/*': entityRouteRef, @@ -107,9 +151,11 @@ describe('', () => { }, ); - expect(rendered.container.innerHTML).toContain( - '/catalog/test-namespace/test/test-name', - ); + await waitFor(() => { + expect( + rendered.getByRole('link', { name: 'test:test-namespace/test-name' }), + ).toHaveAttribute('href', '/catalog/test-namespace/test/test-name'); + }); }); }); }); diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPageHeader/TechDocsReaderPageHeader.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPageHeader/TechDocsReaderPageHeader.tsx index b1ca6ce50a..c417274403 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPageHeader/TechDocsReaderPageHeader.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPageHeader/TechDocsReaderPageHeader.tsx @@ -43,13 +43,12 @@ import { rootRouteRef } from '../../../routes'; const skeleton = ; -export const TechDocsReaderPageHeader: FC = props => { - const { children } = props; +export const TechDocsReaderPageHeader: FC = ({ children }) => { const addons = useTechDocsAddons(); const configApi = useApi(configApiRef); - const { value: techDocsMetadata } = useTechDocsMetadata(); const { value: entityMetadata } = useEntityMetadata(); + const { value: techDocsMetadata } = useTechDocsMetadata(); const { title, @@ -61,11 +60,17 @@ export const TechDocsReaderPageHeader: FC = props => { useEffect(() => { if (!techDocsMetadata) return; - setTitle(prevTitle => prevTitle || techDocsMetadata.site_name); - setSubtitle( - prevSubtitle => - prevSubtitle || techDocsMetadata.site_description || 'Home', - ); + setTitle(prevTitle => { + const { site_name } = techDocsMetadata; + return prevTitle || site_name; + }); + setSubtitle(prevSubtitle => { + let { site_description } = techDocsMetadata; + if (site_description === 'None') { + site_description = 'Home'; + } + return prevSubtitle || site_description; + }); }, [techDocsMetadata, setTitle, setSubtitle]); const appTitle = configApi.getOptional('app.title') || 'Backstage'; diff --git a/plugins/techdocs/src/reader/components/index.ts b/plugins/techdocs/src/reader/components/index.ts index f6294200a3..5abdf002b1 100644 --- a/plugins/techdocs/src/reader/components/index.ts +++ b/plugins/techdocs/src/reader/components/index.ts @@ -19,12 +19,13 @@ export type { TechDocsReaderLayoutProps, } from './TechDocsReaderPage'; export { - TechDocsReaderLayout, - useTechDocsMetadata, - useEntityMetadata, - useTechDocsReaderPage, useShadowRoot, useShadowRootElements, + useEntityMetadata, + useTechDocsMetadata, + useTechDocsReaderPage, + TechDocsReaderLayout, + TechDocsReaderPageProvider, } from './TechDocsReaderPage'; export * from './TechDocsReaderPageHeader'; export * from './TechDocsReaderPageContent';