diff --git a/.changeset/clean-jokes-think.md b/.changeset/clean-jokes-think.md index 4a2c6a2c13..a4fc65e6fc 100644 --- a/.changeset/clean-jokes-think.md +++ b/.changeset/clean-jokes-think.md @@ -1,5 +1,18 @@ --- -'@backstage/plugin-techdocs': minor +'@backstage/plugin-techdocs': patch --- -Rework `TechDocsHome` to use `EntityListProvider` with support for starring docs and filtering on owned, starred, owner, and tags. +Expose a new composable `TechDocsIndexPage` and a `DefaultTechDocsHome` with support for starring docs and filtering on owned, starred, owner, and tags. + +You can migrate to the new UI view by making the following changes in your `App.tsx`: + +```diff +- } /> ++ }> ++ ++ ++ } ++ /> +``` diff --git a/.changeset/plenty-carpets-jog.md b/.changeset/plenty-carpets-jog.md new file mode 100644 index 0000000000..f4586072a7 --- /dev/null +++ b/.changeset/plenty-carpets-jog.md @@ -0,0 +1,18 @@ +--- +'@backstage/create-app': patch +--- + +Use new composable `TechDocsIndexPage` and `DefaultTechDocsHome` + +Make the following changes to your `App.tsx` to migrate existing apps: + +```diff +- } /> ++ }> ++ ++ ++ } ++ /> +``` diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 10fb743304..1132368a91 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -52,7 +52,11 @@ import { } from '@backstage/plugin-scaffolder'; import { SearchPage } from '@backstage/plugin-search'; import { TechRadarPage } from '@backstage/plugin-tech-radar'; -import { TechdocsPage } from '@backstage/plugin-techdocs'; +import { + DefaultTechDocsHome, + TechDocsIndexPage, + TechDocsReaderPage, +} from '@backstage/plugin-techdocs'; import { UserSettingsPage } from '@backstage/plugin-user-settings'; import AlarmIcon from '@material-ui/icons/Alarm'; import React from 'react'; @@ -116,7 +120,13 @@ const routes = ( {entityPage} } /> - } /> + }> + + + } + /> }> diff --git a/packages/create-app/templates/default-app/packages/app/src/App.tsx b/packages/create-app/templates/default-app/packages/app/src/App.tsx index 9b77cf6a95..57491c9222 100644 --- a/packages/create-app/templates/default-app/packages/app/src/App.tsx +++ b/packages/create-app/templates/default-app/packages/app/src/App.tsx @@ -13,7 +13,11 @@ import { import { ScaffolderPage, scaffolderPlugin } from '@backstage/plugin-scaffolder'; import { SearchPage } from '@backstage/plugin-search'; import { TechRadarPage } from '@backstage/plugin-tech-radar'; -import { TechdocsPage } from '@backstage/plugin-techdocs'; +import { + DefaultTechDocsHome, + TechDocsIndexPage, + TechDocsReaderPage, +} from '@backstage/plugin-techdocs'; import { UserSettingsPage } from '@backstage/plugin-user-settings'; import { apis } from './apis'; import { entityPage } from './components/catalog/EntityPage'; @@ -50,7 +54,13 @@ const routes = ( > {entityPage} - } /> + }> + + + } + /> } /> } /> { icon: () => JSX.Element; @@ -50,9 +48,7 @@ function createOwnerColumn(): TableColumn; function createStarEntityAction( isStarredEntity: Function, toggleStarredEntity: Function, -): ({ - entity, -}: DocsTableRow) => { +): ({ entity }: DocsTableRow) => { cellStyle: { paddingLeft: string; }; @@ -66,6 +62,19 @@ function createStarEntityAction( // @public (undocumented) function createTypeColumn(): TableColumn; +// Warning: (ae-missing-release-tag) "DefaultTechDocsHome" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const DefaultTechDocsHome: ({ + initialFilter, + columns, + actions, +}: { + initialFilter?: UserListFilterKind | undefined; + columns?: TableColumn[] | undefined; + actions?: TableProps['actions']; +}) => JSX.Element; + // Warning: (ae-missing-release-tag) "DocsCardGrid" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -235,39 +244,22 @@ export const TechDocsCustomHome: ({ tabsConfig: TabsConfig; }) => JSX.Element; -// Warning: (ae-missing-release-tag) "TechDocsHome" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// Warning: (ae-missing-release-tag) "TechDocsIndexPage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const TechDocsHome: ({ - initialFilter, - columns, - actions, -}: { - initialFilter?: UserListFilterKind | undefined; - columns?: TableColumn[] | undefined; - actions?: - | ( - | Action - | { - action: (rowData: DocsTableRow) => Action; - position: string; - } - | ((rowData: DocsTableRow) => Action) - )[] - | undefined; -}) => JSX.Element; - -// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "TechDocsHomeLayout" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const TechDocsHomeLayout: ({ children }: Props) => JSX.Element; +export const TechDocsIndexPage: () => JSX.Element; // Warning: (ae-missing-release-tag) "TechdocsPage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) export const TechdocsPage: () => JSX.Element; +// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "TechDocsPageWrapper" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const TechDocsPageWrapper: ({ children }: Props) => JSX.Element; + // Warning: (ae-missing-release-tag) "TechDocsPicker" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index 9e15dd7aff..bb9f8a7c95 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -45,7 +45,7 @@ "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", "@material-ui/styles": "^4.10.0", - "@types/react": "^16.9", + "@types/react": "*", "dompurify": "^2.2.9", "event-source-polyfill": "^1.0.25", "lodash": "^4.17.21", @@ -70,7 +70,6 @@ "@types/dompurify": "^2.2.2", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", - "@types/react": "*", "canvas": "^2.6.1", "cross-fetch": "^3.0.6", "msw": "^0.29.0" diff --git a/plugins/techdocs/src/Router.tsx b/plugins/techdocs/src/Router.tsx index 6218f7520d..3fb109e790 100644 --- a/plugins/techdocs/src/Router.tsx +++ b/plugins/techdocs/src/Router.tsx @@ -23,8 +23,8 @@ import { rootDocsRouteRef, rootCatalogDocsRouteRef, } from './routes'; -import { TechDocsHome } from './home/components/TechDocsHome'; -import { TechDocsPage } from './reader/components/TechDocsPage'; +import { TechDocsIndexPage } from './home/components/TechDocsIndexPage'; +import { TechDocsPage as TechDocsReaderPage } from './reader/components/TechDocsPage'; import { EntityPageDocs } from './EntityPageDocs'; import { MissingAnnotationEmptyState } from '@backstage/core-components'; @@ -33,8 +33,11 @@ const TECHDOCS_ANNOTATION = 'backstage.io/techdocs-ref'; export const Router = () => { return ( - } /> - } /> + } /> + } + /> ); }; diff --git a/plugins/techdocs/src/home/components/TechDocsHome.test.tsx b/plugins/techdocs/src/home/components/DefaultTechDocsHome.test.tsx similarity index 95% rename from plugins/techdocs/src/home/components/TechDocsHome.test.tsx rename to plugins/techdocs/src/home/components/DefaultTechDocsHome.test.tsx index 9289884f1c..0b1d6e8db2 100644 --- a/plugins/techdocs/src/home/components/TechDocsHome.test.tsx +++ b/plugins/techdocs/src/home/components/DefaultTechDocsHome.test.tsx @@ -18,7 +18,7 @@ import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react'; import { MockStorageApi, renderInTestApp } from '@backstage/test-utils'; import { screen } from '@testing-library/react'; import React from 'react'; -import { TechDocsHome } from './TechDocsHome'; +import { DefaultTechDocsHome } from './DefaultTechDocsHome'; import { ApiProvider, @@ -71,7 +71,7 @@ describe('TechDocs Home', () => { it('should render a TechDocs home page', async () => { await renderInTestApp( - + , ); diff --git a/plugins/techdocs/src/home/components/TechDocsHome.tsx b/plugins/techdocs/src/home/components/DefaultTechDocsHome.tsx similarity index 93% rename from plugins/techdocs/src/home/components/TechDocsHome.tsx rename to plugins/techdocs/src/home/components/DefaultTechDocsHome.tsx index 0aa1b87282..ff4bf63724 100644 --- a/plugins/techdocs/src/home/components/TechDocsHome.tsx +++ b/plugins/techdocs/src/home/components/DefaultTechDocsHome.tsx @@ -35,11 +35,11 @@ import { UserListPicker, } from '@backstage/plugin-catalog-react'; import { EntityListDocsTable } from './EntityListDocsTable'; -import { TechDocsHomeLayout } from './TechDocsHomeLayout'; +import { TechDocsPageWrapper } from './TechDocsPageWrapper'; import { TechDocsPicker } from './TechDocsPicker'; import { DocsTableRow } from './types'; -export const TechDocsHome = ({ +export const DefaultTechDocsHome = ({ initialFilter = 'all', columns, actions, @@ -49,7 +49,7 @@ export const TechDocsHome = ({ actions?: TableProps['actions']; }) => { return ( - + @@ -70,6 +70,6 @@ export const TechDocsHome = ({ - + ); }; diff --git a/plugins/techdocs/src/home/components/LegacyTechDocsHome.test.tsx b/plugins/techdocs/src/home/components/LegacyTechDocsHome.test.tsx new file mode 100644 index 0000000000..c9a6871906 --- /dev/null +++ b/plugins/techdocs/src/home/components/LegacyTechDocsHome.test.tsx @@ -0,0 +1,82 @@ +/* + * 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 { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react'; +import { renderInTestApp } from '@backstage/test-utils'; +import { screen } from '@testing-library/react'; +import React from 'react'; +import { LegacyTechDocsHome } from './LegacyTechDocsHome'; + +import { + ApiProvider, + ApiRegistry, + ConfigReader, +} from '@backstage/core-app-api'; +import { ConfigApi, configApiRef } from '@backstage/core-plugin-api'; + +jest.mock('@backstage/plugin-catalog-react', () => { + const actual = jest.requireActual('@backstage/plugin-catalog-react'); + return { + ...actual, + useOwnUser: () => 'test-user', + }; +}); + +const mockCatalogApi = { + getEntityByName: jest.fn(), + getEntities: async () => ({ + items: [ + { + apiVersion: 'version', + kind: 'User', + metadata: { + name: 'owned', + namespace: 'default', + }, + }, + ], + }), +} as Partial; + +describe('Legacy TechDocs Home', () => { + const configApi: ConfigApi = new ConfigReader({ + organization: { + name: 'My Company', + }, + }); + + const apiRegistry = ApiRegistry.from([ + [catalogApiRef, mockCatalogApi], + [configApiRef, configApi], + ]); + + it('should render a TechDocs home page', async () => { + await renderInTestApp( + + + , + ); + + // Header + expect(await screen.findByText('Documentation')).toBeInTheDocument(); + expect( + await screen.findByText(/Documentation available in My Company/i), + ).toBeInTheDocument(); + + // Explore Content + expect(await screen.findByTestId('docs-explore')).toBeDefined(); + }); +}); diff --git a/plugins/techdocs/src/home/components/LegacyTechDocsHome.tsx b/plugins/techdocs/src/home/components/LegacyTechDocsHome.tsx new file mode 100644 index 0000000000..34ea416026 --- /dev/null +++ b/plugins/techdocs/src/home/components/LegacyTechDocsHome.tsx @@ -0,0 +1,55 @@ +/* + * 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 React from 'react'; +import { PanelType, TechDocsCustomHome } from './TechDocsCustomHome'; + +export const LegacyTechDocsHome = () => { + const tabsConfig = [ + { + label: 'Overview', + panels: [ + { + title: 'Overview', + description: + 'Explore your internal technical ecosystem through documentation.', + panelType: 'DocsCardGrid' as PanelType, + filterPredicate: () => true, + }, + // uncomment this if you would like to have a secondary panel with owned documents + // { + // title: 'Owned', + // description: 'Explore your owned internal documentation.', + // panelType: 'DocsCardGrid' as PanelType, + // filterPredicate: 'ownedByUser', + // }, + ], + }, + { + label: 'Owned Documents', + panels: [ + { + title: 'Owned documents', + description: 'Access your documentation.', + panelType: 'DocsTable' as PanelType, + // ownedByUser filters out entities owned by signed in user + filterPredicate: 'ownedByUser', + }, + ], + }, + ]; + return ; +}; diff --git a/plugins/techdocs/src/home/components/TechDocsCustomHome.tsx b/plugins/techdocs/src/home/components/TechDocsCustomHome.tsx index 7f93f6a306..9975db6435 100644 --- a/plugins/techdocs/src/home/components/TechDocsCustomHome.tsx +++ b/plugins/techdocs/src/home/components/TechDocsCustomHome.tsx @@ -28,7 +28,7 @@ import { import { Entity } from '@backstage/catalog-model'; import { DocsTable } from './DocsTable'; import { DocsCardGrid } from './DocsCardGrid'; -import { TechDocsHomeLayout } from './TechDocsHomeLayout'; +import { TechDocsPageWrapper } from './TechDocsPageWrapper'; import { CodeSnippet, @@ -149,17 +149,17 @@ export const TechDocsCustomHome = ({ if (loading) { return ( - + - + ); } if (error) { return ( - + - + ); } return ( - + setSelectedTab(index)} @@ -192,6 +192,6 @@ export const TechDocsCustomHome = ({ /> ))} - + ); }; diff --git a/plugins/techdocs/src/home/components/TechDocsIndexPage.test.tsx b/plugins/techdocs/src/home/components/TechDocsIndexPage.test.tsx new file mode 100644 index 0000000000..fbeca7cfd4 --- /dev/null +++ b/plugins/techdocs/src/home/components/TechDocsIndexPage.test.tsx @@ -0,0 +1,44 @@ +/* + * 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 React from 'react'; +import { renderInTestApp } from '@backstage/test-utils'; +import { useOutlet } from 'react-router'; +import { TechDocsIndexPage } from './TechDocsIndexPage'; + +jest.mock('react-router', () => ({ + ...jest.requireActual('react-router'), + useOutlet: jest.fn().mockReturnValue('Route Children'), +})); + +jest.mock('./LegacyTechDocsHome', () => ({ + LegacyTechDocsHome: jest.fn().mockReturnValue('LegacyTechDocsHomeMock'), +})); + +describe('TechDocsIndexPage', () => { + it('renders provided router element', async () => { + const { getByText } = await renderInTestApp(); + + expect(getByText('Route Children')).toBeInTheDocument(); + }); + + it('renders legacy TechDocs home when no router children are provided', async () => { + (useOutlet as jest.Mock).mockReturnValueOnce(null); + const { getByText } = await renderInTestApp(); + + expect(getByText('LegacyTechDocsHomeMock')).toBeInTheDocument(); + }); +}); diff --git a/plugins/techdocs/src/home/components/TechDocsIndexPage.tsx b/plugins/techdocs/src/home/components/TechDocsIndexPage.tsx new file mode 100644 index 0000000000..ff694d3570 --- /dev/null +++ b/plugins/techdocs/src/home/components/TechDocsIndexPage.tsx @@ -0,0 +1,25 @@ +/* + * 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 React from 'react'; +import { useOutlet } from 'react-router'; +import { LegacyTechDocsHome } from './LegacyTechDocsHome'; + +export const TechDocsIndexPage = () => { + const outlet = useOutlet(); + + return outlet || ; +}; diff --git a/plugins/techdocs/src/home/components/TechDocsHomeLayout.tsx b/plugins/techdocs/src/home/components/TechDocsPageWrapper.tsx similarity index 94% rename from plugins/techdocs/src/home/components/TechDocsHomeLayout.tsx rename to plugins/techdocs/src/home/components/TechDocsPageWrapper.tsx index e6717fbecc..453cfc22cc 100644 --- a/plugins/techdocs/src/home/components/TechDocsHomeLayout.tsx +++ b/plugins/techdocs/src/home/components/TechDocsPageWrapper.tsx @@ -23,7 +23,7 @@ type Props = { children?: React.ReactNode; }; -export const TechDocsHomeLayout = ({ children }: Props) => { +export const TechDocsPageWrapper = ({ children }: Props) => { const configApi = useApi(configApiRef); const generatedSubtitle = `Documentation available in ${ configApi.getOptionalString('organization.name') ?? 'Backstage' diff --git a/plugins/techdocs/src/home/components/index.ts b/plugins/techdocs/src/home/components/index.ts index a56b80f9a1..53dd0fd909 100644 --- a/plugins/techdocs/src/home/components/index.ts +++ b/plugins/techdocs/src/home/components/index.ts @@ -15,7 +15,8 @@ */ export { EntityListDocsTable } from './EntityListDocsTable'; -export { TechDocsHomeLayout } from './TechDocsHomeLayout'; +export { DefaultTechDocsHome } from './DefaultTechDocsHome'; +export { TechDocsPageWrapper } from './TechDocsPageWrapper'; export { TechDocsPicker } from './TechDocsPicker'; export type { PanelType } from './TechDocsCustomHome'; export type { DocsTableRow } from './types'; diff --git a/plugins/techdocs/src/index.ts b/plugins/techdocs/src/index.ts index 7c3bf607b4..4ebf29180c 100644 --- a/plugins/techdocs/src/index.ts +++ b/plugins/techdocs/src/index.ts @@ -21,7 +21,8 @@ export { TechDocsClient, TechDocsStorageClient } from './client'; export type { DocsTableRow, PanelType } from './home/components'; export { EntityListDocsTable, - TechDocsHomeLayout, + DefaultTechDocsHome, + TechDocsPageWrapper, TechDocsPicker, } from './home/components'; export * from './components/DocsResultListItem'; @@ -30,7 +31,7 @@ export { DocsTable, EntityTechdocsContent, TechDocsCustomHome, - TechDocsHome, + TechDocsIndexPage, TechdocsPage, techdocsPlugin as plugin, techdocsPlugin, diff --git a/plugins/techdocs/src/plugin.ts b/plugins/techdocs/src/plugin.ts index 2e031176e4..18b386f88c 100644 --- a/plugins/techdocs/src/plugin.ts +++ b/plugins/techdocs/src/plugin.ts @@ -113,10 +113,12 @@ export const TechDocsCustomHome = techdocsPlugin.provide( }), ); -export const TechDocsHome = techdocsPlugin.provide( +export const TechDocsIndexPage = techdocsPlugin.provide( createRoutableExtension({ component: () => - import('./home/components/TechDocsHome').then(m => m.TechDocsHome), + import('./home/components/TechDocsIndexPage').then( + m => m.TechDocsIndexPage, + ), mountPoint: rootRouteRef, }), );