From c8b54c370223d8eb8f9e3ad05681edb8ec368b86 Mon Sep 17 00:00:00 2001 From: Emma Indal Date: Thu, 11 Mar 2021 19:23:32 +0100 Subject: [PATCH] TechDocs Homepage (#4401) * add tabs to homepage Signed-off-by: Emma Indal * add new overview content component Signed-off-by: Emma Indal * add new owned content component Signed-off-by: Emma Indal * add types Signed-off-by: Emma Indal * add test for owned content component Signed-off-by: Emma Indal * add test for overview content Signed-off-by: Emma Indal * document name to link and change copy to clipboard link Signed-off-by: Emma Indal * add changeset Signed-off-by: Emma Indal * rename changeset file Signed-off-by: Emma Indal * fix types Signed-off-by: Emma Indal * move home components out from reader dir to its own dir Signed-off-by: Emma Indal * Update plugins/techdocs/src/reader/components/OverviewContent.tsx Co-authored-by: Himanshu Mishra Signed-off-by: Emma Indal * enable search in table Signed-off-by: Emma Indal * add docs icon to core icons Signed-off-by: Emma Indal * add example TechDocs support button config Signed-off-by: Emma Indal * use isOwnerOff and useOwnUser to see documents that are owned via group membership Signed-off-by: Emma Indal * update tests Signed-off-by: Emma Indal * cleanup imports Signed-off-by: Emma Indal * leave techdocs support config out of this PR Signed-off-by: Emma Indal * prettieeeeeer Signed-off-by: Emma Indal * copy paste useOwnUser hook over to techdocs plugin Signed-off-by: Emma Indal * add changeset Signed-off-by: Emma Indal * delete unused package Signed-off-by: Emma Indal Co-authored-by: Himanshu Mishra --- .changeset/techdocs-tame-spies-kneel.md | 5 + .changeset/tender-keys-study.md | 5 + packages/core-api/src/icons/icons.tsx | 4 + packages/core-api/src/icons/types.ts | 3 +- .../documented-component/catalog-info.yaml | 2 +- plugins/techdocs/src/Router.tsx | 2 +- .../home/components/OverviewContent.test.tsx | 62 ++++++++ .../src/home/components/OverviewContent.tsx | 73 ++++++++++ .../src/home/components/OwnedContent.test.tsx | 109 ++++++++++++++ .../src/home/components/OwnedContent.tsx | 135 ++++++++++++++++++ .../components/TechDocsHome.test.tsx | 0 .../components/TechDocsHome.tsx | 64 ++++----- plugins/techdocs/src/home/hooks/index.ts | 17 +++ plugins/techdocs/src/home/hooks/useOwnUser.ts | 41 ++++++ 14 files changed, 482 insertions(+), 40 deletions(-) create mode 100644 .changeset/techdocs-tame-spies-kneel.md create mode 100644 .changeset/tender-keys-study.md create mode 100644 plugins/techdocs/src/home/components/OverviewContent.test.tsx create mode 100644 plugins/techdocs/src/home/components/OverviewContent.tsx create mode 100644 plugins/techdocs/src/home/components/OwnedContent.test.tsx create mode 100644 plugins/techdocs/src/home/components/OwnedContent.tsx rename plugins/techdocs/src/{reader => home}/components/TechDocsHome.test.tsx (100%) rename plugins/techdocs/src/{reader => home}/components/TechDocsHome.tsx (60%) create mode 100644 plugins/techdocs/src/home/hooks/index.ts create mode 100644 plugins/techdocs/src/home/hooks/useOwnUser.ts diff --git a/.changeset/techdocs-tame-spies-kneel.md b/.changeset/techdocs-tame-spies-kneel.md new file mode 100644 index 0000000000..2b69c41213 --- /dev/null +++ b/.changeset/techdocs-tame-spies-kneel.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +Extended TechDocs HomePage with owned documents diff --git a/.changeset/tender-keys-study.md b/.changeset/tender-keys-study.md new file mode 100644 index 0000000000..a3adcbf783 --- /dev/null +++ b/.changeset/tender-keys-study.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-api': patch +--- + +Added new Docs Icon to Core Icons diff --git a/packages/core-api/src/icons/icons.tsx b/packages/core-api/src/icons/icons.tsx index 49db90efcc..e638131a28 100644 --- a/packages/core-api/src/icons/icons.tsx +++ b/packages/core-api/src/icons/icons.tsx @@ -24,6 +24,8 @@ import MuiHelpIcon from '@material-ui/icons/Help'; import MuiPeopleIcon from '@material-ui/icons/People'; import MuiPersonIcon from '@material-ui/icons/Person'; import MuiWarningIcon from '@material-ui/icons/Warning'; +import MuiDocsIcon from '@material-ui/icons/Description'; + import React from 'react'; import { useApp } from '../app/AppContext'; import { IconComponent, IconComponentMap, SystemIconKey } from './types'; @@ -38,6 +40,7 @@ export const defaultSystemIcons: IconComponentMap = { help: MuiHelpIcon, user: MuiPersonIcon, warning: MuiWarningIcon, + docs: MuiDocsIcon, }; const overridableSystemIcon = (key: SystemIconKey): IconComponent => { @@ -58,3 +61,4 @@ export const GroupIcon = overridableSystemIcon('group'); export const HelpIcon = overridableSystemIcon('help'); export const UserIcon = overridableSystemIcon('user'); export const WarningIcon = overridableSystemIcon('warning'); +export const DocsIcon = overridableSystemIcon('docs'); diff --git a/packages/core-api/src/icons/types.ts b/packages/core-api/src/icons/types.ts index c4634bd00a..f2a9a3ba59 100644 --- a/packages/core-api/src/icons/types.ts +++ b/packages/core-api/src/icons/types.ts @@ -26,7 +26,8 @@ export type SystemIconKey = | 'group' | 'help' | 'user' - | 'warning'; + | 'warning' + | 'docs'; export type IconComponent = ComponentType; export type IconKey = SystemIconKey | string; diff --git a/plugins/techdocs-backend/examples/documented-component/catalog-info.yaml b/plugins/techdocs-backend/examples/documented-component/catalog-info.yaml index d609866f9b..ccdf8d3183 100644 --- a/plugins/techdocs-backend/examples/documented-component/catalog-info.yaml +++ b/plugins/techdocs-backend/examples/documented-component/catalog-info.yaml @@ -8,4 +8,4 @@ metadata: spec: type: service lifecycle: experimental - owner: documented@example.com + owner: user:guest diff --git a/plugins/techdocs/src/Router.tsx b/plugins/techdocs/src/Router.tsx index b220912174..752ce33eec 100644 --- a/plugins/techdocs/src/Router.tsx +++ b/plugins/techdocs/src/Router.tsx @@ -24,7 +24,7 @@ import { rootDocsRouteRef, rootCatalogDocsRouteRef, } from './plugin'; -import { TechDocsHome } from './reader/components/TechDocsHome'; +import { TechDocsHome } from './home/components/TechDocsHome'; import { TechDocsPage } from './reader/components/TechDocsPage'; import { EntityPageDocs } from './EntityPageDocs'; diff --git a/plugins/techdocs/src/home/components/OverviewContent.test.tsx b/plugins/techdocs/src/home/components/OverviewContent.test.tsx new file mode 100644 index 0000000000..ec67c346d7 --- /dev/null +++ b/plugins/techdocs/src/home/components/OverviewContent.test.tsx @@ -0,0 +1,62 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { wrapInTestApp } from '@backstage/test-utils'; +import { render } from '@testing-library/react'; +import React from 'react'; +import { OverviewContent } from './OverviewContent'; + +describe('TechDocs Overview Content', () => { + it('should render all TechDocs Documents', async () => { + const { findByText } = render( + wrapInTestApp( + , + ), + ); + + expect(await findByText('Overview')).toBeInTheDocument(); + expect( + await findByText( + /Explore your internal technical ecosystem through documentation./i, + ), + ).toBeInTheDocument(); + expect(await findByText('testName')).toBeInTheDocument(); + expect(await findByText('testName2')).toBeInTheDocument(); + }); +}); diff --git a/plugins/techdocs/src/home/components/OverviewContent.tsx b/plugins/techdocs/src/home/components/OverviewContent.tsx new file mode 100644 index 0000000000..e0030691f5 --- /dev/null +++ b/plugins/techdocs/src/home/components/OverviewContent.tsx @@ -0,0 +1,73 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { generatePath } from 'react-router-dom'; + +import { Entity } from '@backstage/catalog-model'; +import { + Button, + Content, + ContentHeader, + SupportButton, + ItemCardGrid, + ItemCardHeader, +} from '@backstage/core'; +import { Card, CardActions, CardContent, CardMedia } from '@material-ui/core'; + +import { rootDocsRouteRef } from '../../plugin'; + +export const OverviewContent = ({ + entities, +}: { + entities: Entity[] | undefined; +}) => { + if (!entities) return null; + return ( + + + Discover documentation in your ecosystem. + + + {!entities?.length + ? null + : entities.map((entity, index: number) => ( + + + + + {entity.metadata.description} + + + + + ))} + + + ); +}; diff --git a/plugins/techdocs/src/home/components/OwnedContent.test.tsx b/plugins/techdocs/src/home/components/OwnedContent.test.tsx new file mode 100644 index 0000000000..edccb0d3c6 --- /dev/null +++ b/plugins/techdocs/src/home/components/OwnedContent.test.tsx @@ -0,0 +1,109 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { render } from '@testing-library/react'; +import { wrapInTestApp } from '@backstage/test-utils'; +import { OwnedContent } from './OwnedContent'; + +jest.mock('../hooks', () => ({ + useOwnUser: () => { + return { + value: { + apiVersion: 'version', + kind: 'User', + metadata: { + name: 'owned', + namespace: 'default', + }, + relations: [ + { + target: { + kind: 'TestKind', + name: 'testName', + }, + type: 'ownerOf', + }, + ], + }, + }; + }, +})); + +describe('TechDocs Owned Content', () => { + it('should render TechDocs Owned Documents', async () => { + const { findByText, queryByText } = render( + wrapInTestApp( + , + ), + ); + + expect(await findByText('Owned documents')).toBeInTheDocument(); + expect(await findByText(/Access your documentation./i)).toBeInTheDocument(); + expect(await findByText('testName')).toBeInTheDocument(); + expect(await queryByText('testName2')).not.toBeInTheDocument(); + }); + + it('should render empty state if no owned documents exist', async () => { + const { findByText } = render( + wrapInTestApp(), + ); + + expect(await findByText('No documents to show')).toBeInTheDocument(); + }); +}); diff --git a/plugins/techdocs/src/home/components/OwnedContent.tsx b/plugins/techdocs/src/home/components/OwnedContent.tsx new file mode 100644 index 0000000000..f962147345 --- /dev/null +++ b/plugins/techdocs/src/home/components/OwnedContent.tsx @@ -0,0 +1,135 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { useCopyToClipboard } from 'react-use'; +import { generatePath } from 'react-router-dom'; + +import { IconButton, Tooltip } from '@material-ui/core'; +import ShareIcon from '@material-ui/icons/Share'; +import { + Content, + ContentHeader, + SupportButton, + Table, + EmptyState, + Button, + SubvalueCell, + Link, +} from '@backstage/core'; +import { Entity } from '@backstage/catalog-model'; +import { isOwnerOf } from '@backstage/plugin-catalog-react'; +import { rootDocsRouteRef } from '../../plugin'; +import { useOwnUser } from '../hooks'; + +export const OwnedContent = ({ + entities, +}: { + entities: Entity[] | undefined; +}) => { + const [, copyToClipboard] = useCopyToClipboard(); + const { value: user } = useOwnUser(); + + if (!entities || !user) return null; + + const ownedDocuments = entities + .filter((entity: Entity) => isOwnerOf(user, entity)) + .map(entity => { + return { + name: entity.metadata.name, + description: entity.metadata.description, + owner: entity?.spec?.owner, + type: entity?.spec?.type, + docsUrl: generatePath(rootDocsRouteRef.path, { + namespace: entity.metadata.namespace ?? 'default', + kind: entity.kind, + name: entity.metadata.name, + }), + }; + }); + + const columns = [ + { + title: 'Document', + field: 'name', + highlight: true, + render: (row: any): React.ReactNode => ( + {row.name}} + subvalue={row.description} + /> + ), + }, + { + title: 'Owner', + field: 'owner', + }, + { + title: 'Type', + field: 'type', + }, + { + title: 'Actions', + width: '10%', + render: (row: any) => ( + + + copyToClipboard(`${window.location.origin}/${row.docsUrl}`) + } + > + + + + ), + }, + ]; + + return ( + + + Discover documentation you own. + + {ownedDocuments && ownedDocuments.length > 0 ? ( + + ) : ( + + DOCS + + } + /> + )} + + ); +}; diff --git a/plugins/techdocs/src/reader/components/TechDocsHome.test.tsx b/plugins/techdocs/src/home/components/TechDocsHome.test.tsx similarity index 100% rename from plugins/techdocs/src/reader/components/TechDocsHome.test.tsx rename to plugins/techdocs/src/home/components/TechDocsHome.test.tsx diff --git a/plugins/techdocs/src/reader/components/TechDocsHome.tsx b/plugins/techdocs/src/home/components/TechDocsHome.tsx similarity index 60% rename from plugins/techdocs/src/reader/components/TechDocsHome.tsx rename to plugins/techdocs/src/home/components/TechDocsHome.tsx index 6b8e721b94..916a46ab7a 100644 --- a/plugins/techdocs/src/reader/components/TechDocsHome.tsx +++ b/plugins/techdocs/src/home/components/TechDocsHome.tsx @@ -14,31 +14,34 @@ * limitations under the License. */ +import React, { useState } from 'react'; +import { useAsync } from 'react-use'; + +import { catalogApiRef, CatalogApi } from '@backstage/plugin-catalog-react'; +import { Entity } from '@backstage/catalog-model'; import { - Button, CodeSnippet, Content, Header, - ItemCardGrid, - ItemCardHeader, + HeaderTabs, Page, Progress, useApi, WarningPanel, } from '@backstage/core'; -import { catalogApiRef } from '@backstage/plugin-catalog-react'; -import { Card, CardActions, CardContent, CardMedia } from '@material-ui/core'; -import React from 'react'; -import { generatePath } from 'react-router-dom'; -import { useAsync } from 'react-use'; -import { rootDocsRouteRef } from '../../plugin'; + +import { OverviewContent } from './OverviewContent'; +import { OwnedContent } from './OwnedContent'; export const TechDocsHome = () => { - const catalogApi = useApi(catalogApiRef); + const [selectedTab, setSelectedTab] = useState(0); + const catalogApi: CatalogApi = useApi(catalogApiRef); + + const tabs = [{ label: 'Overview' }, { label: 'Owned Documents' }]; const { value, loading, error } = useAsync(async () => { const response = await catalogApi.getEntities(); - return response.items.filter(entity => { + return response.items.filter((entity: Entity) => { return !!entity.metadata.annotations?.['backstage.io/techdocs-ref']; }); }); @@ -82,32 +85,19 @@ export const TechDocsHome = () => { title="Documentation" subtitle="Documentation available in Backstage" /> - - - {!value?.length - ? null - : value.map((entity, index: number) => ( - - - - - {entity.metadata.description} - - - - - ))} - - + setSelectedTab(index)} + tabs={tabs.map(({ label }, index) => ({ + id: index.toString(), + label, + }))} + /> + {selectedTab === 0 ? ( + + ) : ( + + )} ); }; diff --git a/plugins/techdocs/src/home/hooks/index.ts b/plugins/techdocs/src/home/hooks/index.ts new file mode 100644 index 0000000000..96419f5e7b --- /dev/null +++ b/plugins/techdocs/src/home/hooks/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { useOwnUser } from './useOwnUser'; diff --git a/plugins/techdocs/src/home/hooks/useOwnUser.ts b/plugins/techdocs/src/home/hooks/useOwnUser.ts new file mode 100644 index 0000000000..29d8a0d11f --- /dev/null +++ b/plugins/techdocs/src/home/hooks/useOwnUser.ts @@ -0,0 +1,41 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { UserEntity } from '@backstage/catalog-model'; +import { identityApiRef, useApi } from '@backstage/core'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; +import { useAsync } from 'react-use'; +import { AsyncState } from 'react-use/lib/useAsync'; + +/** + * Get the catalog User entity (if any) that matches the logged-in user. + */ +export function useOwnUser(): AsyncState { + const catalogApi = useApi(catalogApiRef); + const identityApi = useApi(identityApiRef); + + // TODO: get the full entity (or at least the full entity name) from the + // identityApi + return useAsync( + () => + catalogApi.getEntityByName({ + kind: 'User', + namespace: 'default', + name: identityApi.getUserId(), + }) as Promise, + [catalogApi, identityApi], + ); +}