diff --git a/plugins/techdocs-addons/README.md b/plugins/techdocs-addons/README.md index b97a8d583d..432aa757e4 100644 --- a/plugins/techdocs-addons/README.md +++ b/plugins/techdocs-addons/README.md @@ -1,5 +1,61 @@ # @backstage/plugin-techdocs-addons -Package encapsulating the TechDocs Addons framework. +Package encapsulating the TechDocs Addon framework. -todo(backstage/techdocs-core): Fill in with real documentation! +## What is an addon? + +An addon is a isolated piece of functionality that one can use to augment the +TechDocs experience at render-time. For example: an issue counter showing the +number of issues reported on the documentation, or the top contributors to the +documentation. + +## Create a new addon + +To create a new addon, you can use the `createTechDocsAddon` factory exported +from this plugin. Normally, addons are provided by Backstage plugins, which can +then be composed within a Backstage app. + +When you create a new Addon, it requires three things. + +1. A `name` for debugging and analytics purposes) +2. A `location`, indicating where/how the addon will be rendered +3. A `component`, encapsulating the addon's logic and functionality + +```tsx +import { + createTechDocsAddon, + TechDocsAddonLocations, +} from '@backstage/plugin-techdocs-addons'; +import { StackOverflowSecondarySidebarAddon } from './components'; + +export const StackOverflowSecondarySidebar = yourBackstagePlugin.provide( + createTechDocsAddon({ + name: 'StackOverflowSecondarySidebar', + type: TechDocsAddonLocations.SECONDARY_SIDEBAR, + component: StackOverflowSecondarySidebarAddon, + }), +); +``` + +## Compose your app with addons + +To configure which addons will augment the TechDocs experience in your +Backstage app, you need two things: + +- The `TechDocsAddons` component, which is responsible for registering the + addons. +- A list of the addons themselves, as exported by their respective plugins. + +```tsx +import { + TechDocsAddons, + TechDocsReaderPage, +} from '@backstage/plugin-techdocs-addons'; +import { StackOverflowSecondarySidebar } from '@backstage/plugin-soe'; + +}> + + + +; +``` diff --git a/plugins/techdocs-addons/api-report.md b/plugins/techdocs-addons/api-report.md index e14ea6112c..f3b3d5fe69 100644 --- a/plugins/techdocs-addons/api-report.md +++ b/plugins/techdocs-addons/api-report.md @@ -4,6 +4,13 @@ ```ts import { ComponentType } from 'react'; +import { Extension } from '@backstage/core-plugin-api'; +import { default as React_2 } from 'react'; + +// @public +export function createTechDocsAddon( + options: TechDocsAddonOptions, +): Extension>; // @public export enum TechDocsAddonLocations { @@ -21,4 +28,7 @@ export type TechDocsAddonOptions = { location: TechDocsAddonLocations; component: ComponentType; }; + +// @public +export const TechDocsAddons: React_2.ComponentType; ``` diff --git a/plugins/techdocs-addons/package.json b/plugins/techdocs-addons/package.json index 4df0488689..79da76e32a 100644 --- a/plugins/techdocs-addons/package.json +++ b/plugins/techdocs-addons/package.json @@ -21,7 +21,10 @@ "prepack": "backstage-cli package prepack", "postpack": "backstage-cli package postpack" }, - "dependencies": {}, + "dependencies": { + "@backstage/core-plugin-api": "^0.8.0", + "react-router-dom": "6.0.0-beta.0" + }, "peerDependencies": { "@types/react": "^16.13.1 || ^17.0.0", "react": "^16.13.1 || ^17.0.0" diff --git a/plugins/techdocs-addons/src/addons.tsx b/plugins/techdocs-addons/src/addons.tsx new file mode 100644 index 0000000000..36b5d767ba --- /dev/null +++ b/plugins/techdocs-addons/src/addons.tsx @@ -0,0 +1,120 @@ +/* + * 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 { + attachComponentData, + createReactExtension, + ElementCollection, + Extension, + useElementFilter, +} from '@backstage/core-plugin-api'; +import React, { ComponentType, useCallback } from 'react'; +import { useOutlet } from 'react-router-dom'; + +import { TechDocsAddonLocations, TechDocsAddonOptions } from './types'; + +export const TECHDOCS_ADDONS_KEY = 'techdocs.addons.addon.v1'; +export const TECHDOCS_ADDONS_WRAPPER_KEY = 'techdocs.addons.wrapper.v1'; + +/** + * TechDocs Addon registry. + * @public + */ +export const TechDocsAddons: React.ComponentType = () => null; + +attachComponentData(TechDocsAddons, TECHDOCS_ADDONS_WRAPPER_KEY, true); + +const getDataKeyByName = (name: string) => { + return `${TECHDOCS_ADDONS_KEY}.${name.toLocaleLowerCase('en-US')}`; +}; + +/** + * Create a TechDocs addon. + * @public + */ +export function createTechDocsAddon( + options: TechDocsAddonOptions, +): Extension> { + const { name, component: TechDocsAddon } = options; + return createReactExtension({ + name, + component: { + sync: (props: TComponentProps) => , + }, + data: { + [TECHDOCS_ADDONS_KEY]: options, + [getDataKeyByName(name)]: true, + }, + }); +} + +const getTechDocsAddonByName = (collection: ElementCollection, key: string) => { + return collection.selectByComponentData({ key }).getElements()[0]; +}; + +const getAllTechDocsAddons = (collection: ElementCollection) => { + return collection + .selectByComponentData({ + key: TECHDOCS_ADDONS_WRAPPER_KEY, + }) + .selectByComponentData({ + key: TECHDOCS_ADDONS_KEY, + }); +}; + +const getAllTechDocsAddonsData = (collection: ElementCollection) => { + return collection + .selectByComponentData({ + key: TECHDOCS_ADDONS_WRAPPER_KEY, + }) + .findComponentData({ + key: TECHDOCS_ADDONS_KEY, + }); +}; + +export const useTechDocsAddons = () => { + const node = useOutlet(); + + const collection = useElementFilter(node, getAllTechDocsAddons); + const options = useElementFilter(node, getAllTechDocsAddonsData); + + const findAddonByData = useCallback( + (data: TechDocsAddonOptions | undefined) => { + if (!collection || !data) return null; + const nameKey = getDataKeyByName(data.name); + return getTechDocsAddonByName(collection, nameKey) ?? null; + }, + [collection], + ); + + const renderComponentWithName = useCallback( + (name: string) => { + const data = options.find(option => option.name === name); + return data ? findAddonByData(data) : null; + }, + [options, findAddonByData], + ); + + const renderComponentsWithLocation = useCallback( + (location: TechDocsAddonLocations) => { + const data = options.filter(option => option.location === location); + return data.length ? data.map(findAddonByData) : null; + }, + [options, findAddonByData], + ); + + return { renderComponentWithName, renderComponentsWithLocation }; +}; diff --git a/plugins/techdocs-addons/src/index.ts b/plugins/techdocs-addons/src/index.ts index 715da10e20..ba9f2a7266 100644 --- a/plugins/techdocs-addons/src/index.ts +++ b/plugins/techdocs-addons/src/index.ts @@ -20,4 +20,5 @@ * @packageDocumentation */ +export { createTechDocsAddon, TechDocsAddons } from './addons'; export type { TechDocsAddonLocations, TechDocsAddonOptions } from './types';