diff --git a/packages/techdocs-addons/.eslintrc.js b/packages/techdocs-addons/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/packages/techdocs-addons/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/techdocs-addons/README.md b/packages/techdocs-addons/README.md new file mode 100644 index 0000000000..050adbbaef --- /dev/null +++ b/packages/techdocs-addons/README.md @@ -0,0 +1,11 @@ +# @backstage/techdocs-addons + +This package provides a TechDocs Addons framework used to create and consume TechDocs Addons. + +## Installation + +Install the package: + +```sh +yarn add @backstage/techdocs-addons +``` diff --git a/packages/techdocs-addons/package.json b/packages/techdocs-addons/package.json new file mode 100644 index 0000000000..36e508918f --- /dev/null +++ b/packages/techdocs-addons/package.json @@ -0,0 +1,59 @@ +{ + "name": "@backstage/techdocs-addons", + "description": "TechDocs Addons Framework", + "version": "0.0.0", + "private": false, + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "web-library" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "packages/techdocs-addons" + }, + "keywords": [ + "backstage", + "techdocs" + ], + "license": "Apache-2.0", + "main": "src/index.ts", + "types": "src/index.ts", + "scripts": { + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean", + "start": "backstage-cli package start" + }, + "dependencies": { + "@backstage/catalog-model": "^0.13.0", + "@backstage/core-components": "^0.9.1", + "@backstage/core-plugin-api": "^0.8.0", + "@material-ui/core": "^4.12.2", + "@material-ui/lab": "4.0.0-alpha.57", + "@material-ui/styles": "^4.11.0", + "jss": "~10.8.2", + "react-helmet": "6.1.0", + "react-router-dom": "6.0.0-beta.0", + "react-use": "^17.2.4" + }, + "peerDependencies": { + "@types/react": "^16.13.1 || ^17.0.0", + "react": "^16.13.1 || ^17.0.0" + }, + "devDependencies": { + "@testing-library/react-hooks": "^7.0.2", + "@backstage/test-utils": "^0.3.0" + }, + "files": [ + "dist" + ] +} diff --git a/packages/techdocs-addons/src/addons.tsx b/packages/techdocs-addons/src/addons.tsx new file mode 100644 index 0000000000..0eb102aa24 --- /dev/null +++ b/packages/techdocs-addons/src/addons.tsx @@ -0,0 +1,125 @@ +/* + * 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, { ComponentType, useCallback } from 'react'; +import { useOutlet } from 'react-router-dom'; + +import { + attachComponentData, + createReactExtension, + ElementCollection, + Extension, + useElementFilter, +} from '@backstage/core-plugin-api'; + +import { TechDocsAddonLocations, TechDocsAddonOptions } from './types'; + +export const TECHDOCS_ADDONS_KEY = 'techdocs.addons.addon.v1'; + +/** + * Marks the registry component. + * @public + */ +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 renderComponentByName = useCallback( + (name: string) => { + const data = options.find(option => option.name === name); + return data ? findAddonByData(data) : null; + }, + [options, findAddonByData], + ); + + const renderComponentsByLocation = useCallback( + (location: TechDocsAddonLocations) => { + const data = options.filter(option => option.location === location); + return data.length ? data.map(findAddonByData) : null; + }, + [options, findAddonByData], + ); + + return { renderComponentByName, renderComponentsByLocation }; +}; diff --git a/packages/techdocs-addons/src/index.ts b/packages/techdocs-addons/src/index.ts new file mode 100644 index 0000000000..2ef7265db3 --- /dev/null +++ b/packages/techdocs-addons/src/index.ts @@ -0,0 +1,30 @@ +/* + * 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. + */ + +/** + * Package encapsulating the TechDocs Addon framework. + * + * @packageDocumentation + */ + +export { + useTechDocsAddons, + createTechDocsAddon, + TechDocsAddons, + TECHDOCS_ADDONS_WRAPPER_KEY, +} from './addons'; +export { TechDocsAddonLocations } from './types'; +export type { TechDocsAddonAsyncMetadata, TechDocsAddonOptions } from './types'; diff --git a/packages/techdocs-addons/src/types.ts b/packages/techdocs-addons/src/types.ts new file mode 100644 index 0000000000..2eb8a2f57e --- /dev/null +++ b/packages/techdocs-addons/src/types.ts @@ -0,0 +1,80 @@ +/* + * 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 { Entity } from '@backstage/catalog-model'; +import { ComponentType } from 'react'; +import { AsyncState } from 'react-use/lib/useAsyncFn'; + +/** + * Locations for which TechDocs addons may be declared and rendered. + * @public + */ +export enum TechDocsAddonLocations { + /** + * These addons fill up the header from the right, on the same line as the + * title. + */ + HEADER = 'header', + + /** + * These addons appear below the header and above all content; tooling addons + * can be inserted for convenience. + */ + SUBHEADER = 'subheader', + + /** + * These addons appear left of the content and above the navigation. + */ + PRIMARY_SIDEBAR = 'primary sidebar', + + /** + * These addons appear right of the content and above the table of contents. + */ + SECONDARY_SIDEBAR = 'secondary sidebar', + + /** + * A virtual location which allows mutation of all content within the shadow + * root by transforming DOM nodes. These addons should return null on render. + */ + CONTENT = 'content', + + /** + * A virtual location allowing an instance of the addon to be rendered for + * every HTML node with the same tag name as the addon name in the markdown + * content. If no reference is made, no instance will be rendered. Works like + * regular React components, just being accessible from markdown. + * + * todo(backstage/techdocs-core): Keep and implement or remove before + * releasing this package! + */ + COMPONENT = 'component', +} + +/** + * Options for creating a TechDocs addon. + * @public + */ +export type TechDocsAddonOptions = { + name: string; + location: TechDocsAddonLocations; + component: ComponentType; +}; + +/** + * Common response envelope for addon-related hooks. + * @public + */ +export type TechDocsAddonAsyncMetadata = AsyncState;