feat(techdocs): delete addons plugin

Co-authored-by: Emma Indal <emma.indahl@gmail.com>
Signed-off-by: Camila Belo <camilaibs@gmail.com>
This commit is contained in:
Camila Belo
2022-03-29 20:09:17 +02:00
committed by Emma Indal
parent 24581a2fd3
commit 236089c6cd
23 changed files with 0 additions and 1523 deletions
-1
View File
@@ -1 +0,0 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
-64
View File
@@ -1,64 +0,0 @@
# @backstage/plugin-techdocs-addons
Package encapsulating the TechDocs Addon framework.
## 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. Valid
locations include: `header`, `subheader`, `primary sidebar`,
`secondary sidebar`, `content`, and `component`. Values are available on an
enumerable `TechDocsAddonLocations` and are type-hinted.
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',
location: 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';
<Route path="/docs/:namespace/:kind/:name/*" element={<TechDocsReaderPage />}>
<TechDocsAddons>
<StackOverflowSecondarySidebar />
</TechDocsAddons>
</Route>;
```
-86
View File
@@ -1,86 +0,0 @@
## API Report File for "@backstage/plugin-techdocs-addons"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { AsyncState } from 'react-use/lib/useAsyncFn';
import { ComponentType } from 'react';
import { Entity } from '@backstage/catalog-model';
import { Extension } from '@backstage/core-plugin-api';
import { default as React_2 } from 'react';
// @public
export function createTechDocsAddon<TComponentProps>(
options: TechDocsAddonOptions<TComponentProps>,
): Extension<ComponentType<TComponentProps>>;
// @public
export const TECHDOCS_ADDONS_WRAPPER_KEY = 'techdocs.addons.wrapper.v1';
// @public
export type TechDocsAddonAsyncMetadata<TValue> = AsyncState<TValue | undefined>;
// @public
export enum TechDocsAddonLocations {
COMPONENT = 'component',
CONTENT = 'content',
HEADER = 'header',
PRIMARY_SIDEBAR = 'primary sidebar',
SECONDARY_SIDEBAR = 'secondary sidebar',
SUBHEADER = 'subheader',
}
// @public
export type TechDocsAddonOptions<TAddonProps = {}> = {
name: string;
location: TechDocsAddonLocations;
component: ComponentType<TAddonProps>;
};
// @public
export const TechDocsAddons: React_2.ComponentType;
// @public
export type TechDocsEntityMetadata = Entity & {
locationMetadata?: {
type: string;
target: string;
};
};
// @public
export type TechDocsMetadata = {
site_name: string;
site_description: string;
};
// @public
export const TechDocsReaderPage: (
props: TechDocsReaderPageProps,
) => JSX.Element;
// @public (undocumented)
export type TechDocsReaderPageProps = {
hideHeader?: boolean;
addonConfig?: React_2.ReactNode;
dom: Element | null;
asyncEntityMetadata: AsyncState<TechDocsEntityMetadata>;
asyncTechDocsMetadata: AsyncState<TechDocsMetadata>;
};
// @public
export const useEntityMetadata: () => TechDocsAddonAsyncMetadata<TechDocsEntityMetadata>;
// @public
export const useShadowRoot: () => ShadowRoot | undefined;
// @public
export const useShadowRootElements: <
TReturnedElement extends HTMLElement = HTMLElement,
>(
selectors: string[],
) => TReturnedElement[];
// @public
export const useTechDocsMetadata: () => TechDocsAddonAsyncMetadata<TechDocsMetadata>;
```
-53
View File
@@ -1,53 +0,0 @@
{
"name": "@backstage/plugin-techdocs-addons",
"version": "0.0.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
"types": "dist/index.d.ts"
},
"backstage": {
"role": "frontend-plugin"
},
"scripts": {
"start": "backstage-cli package start",
"build": "backstage-cli package build",
"lint": "backstage-cli package lint",
"test": "backstage-cli package test",
"clean": "backstage-cli package clean",
"prepack": "backstage-cli package prepack",
"postpack": "backstage-cli package postpack"
},
"dependencies": {
"@backstage/catalog-model": "^0.13.0",
"@backstage/core-components": "^0.9.1",
"@backstage/core-plugin-api": "^0.8.0",
"@backstage/test-utils": "^0.3.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",
"lodash.debounce": "^4.0.8",
"react-dom": "^17.0.2",
"react-helmet": "6.1.0",
"react-router-dom": "6.0.0-beta.0",
"react-use": "^17.2.4",
"testing-library__dom": "^7.29.4-beta.1",
"@testing-library/react": "^12.1.3"
},
"peerDependencies": {
"@types/react": "^16.13.1 || ^17.0.0",
"react": "^16.13.1 || ^17.0.0"
},
"devDependencies": {
"@testing-library/react-hooks": "^7.0.2",
"@testing-library/jest-dom": "^5.10.1",
"@types/lodash.debounce": "^4.0.6"
},
"files": [
"dist"
]
}
-153
View File
@@ -1,153 +0,0 @@
/*
* 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,
createContext,
PropsWithChildren,
useCallback,
useContext,
} from 'react';
import { useOutlet } from 'react-router-dom';
import { TechDocsAddonLocations, TechDocsAddonOptions } from './types';
export const TECHDOCS_ADDONS_KEY = 'techdocs.addons.addon.v1';
/**
* Marks the <TechDocsAddons> 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<TComponentProps>(
options: TechDocsAddonOptions<TComponentProps>,
): Extension<ComponentType<TComponentProps>> {
const { name, component: TechDocsAddon } = options;
return createReactExtension({
name,
component: {
sync: (props: TComponentProps) => <TechDocsAddon {...props} />,
},
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<TechDocsAddonOptions>({
key: TECHDOCS_ADDONS_KEY,
});
};
type TechDocsAddonConfig = {
config?: React.ReactNode | null;
};
const TechDocsAddonConfigContext = createContext<TechDocsAddonConfig>({});
export const TechDocsAddonConfigProvider = (
props: PropsWithChildren<{ config?: React.ReactNode }>,
) => {
const fromOutlet = useOutlet();
const config = props.config ?? fromOutlet;
return (
<TechDocsAddonConfigContext.Provider value={{ config }}>
{props.children}
</TechDocsAddonConfigContext.Provider>
);
};
const useTechDocsAddonsConfig = (): React.ReactNode | null => {
return useContext(TechDocsAddonConfigContext).config || null;
};
export const useTechDocsAddons = () => {
const node = useTechDocsAddonsConfig();
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 };
};
@@ -1,75 +0,0 @@
/*
* 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 { Page } from '@backstage/core-components';
import React from 'react';
import { useParams } from 'react-router-dom';
import { AsyncState } from 'react-use/lib/useAsyncFn';
import { TechDocsAddonConfigProvider } from '../../addons';
import {
TechDocsMetadataProvider,
TechDocsEntityProvider,
TechDocsReaderPageProvider,
} from '../../context';
import { TechDocsEntityMetadata, TechDocsMetadata } from '../../types';
import { TechDocsReaderPageContent } from '../TechDocsReaderPageContent';
import { TechDocsReaderPageHeader } from '../TechDocsReaderPageHeader';
import { TechDocsReaderPageSubheader } from '../TechDocsReaderPageSubheader';
/**
* @public
*/
export type TechDocsReaderPageProps = {
hideHeader?: boolean;
addonConfig?: React.ReactNode;
dom: Element | null;
asyncEntityMetadata: AsyncState<TechDocsEntityMetadata>;
asyncTechDocsMetadata: AsyncState<TechDocsMetadata>;
};
/**
* An addon-aware implementation of the TechDocsReaderPage.
* @public
*/
export const TechDocsReaderPage = (props: TechDocsReaderPageProps) => {
const {
addonConfig,
asyncEntityMetadata,
asyncTechDocsMetadata,
dom,
hideHeader = false,
} = props;
const { namespace, kind, name } = useParams();
const entityName = { namespace, kind, name };
return (
<TechDocsMetadataProvider asyncValue={asyncTechDocsMetadata}>
<TechDocsEntityProvider asyncValue={asyncEntityMetadata}>
<TechDocsAddonConfigProvider config={addonConfig}>
<TechDocsReaderPageProvider entityName={entityName}>
<Page themeId="documentation">
{!hideHeader && <TechDocsReaderPageHeader />}
<TechDocsReaderPageSubheader />
{/* todo(backstage/techdocs-core): handle state indicator */}
{/* <TechDocReaderPageIndicator /> */}
<TechDocsReaderPageContent dom={dom} />
</Page>
</TechDocsReaderPageProvider>
</TechDocsAddonConfigProvider>
</TechDocsEntityProvider>
</TechDocsMetadataProvider>
);
};
@@ -1,18 +0,0 @@
/*
* 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.
*/
export { TechDocsReaderPage } from './TechDocsReaderPage';
export type { TechDocsReaderPageProps } from './TechDocsReaderPage';
@@ -1,98 +0,0 @@
/*
* 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 { Content, Progress } from '@backstage/core-components';
import { Portal } from '@material-ui/core';
import { StylesProvider, jssPreset } from '@material-ui/styles';
import React, { useEffect, useRef, useState } from 'react';
import { create } from 'jss';
import { useTechDocsAddons } from '../../addons';
import { useTechDocsReaderPage } from '../../context';
import { TechDocsAddonLocations as locations } from '../../types';
export const TechDocsReaderPageContent = ({ dom }: { dom: Element | null }) => {
const ref = useRef<HTMLDivElement>(null);
const [jss, setJss] = useState(
create({
...jssPreset(),
insertionPoint: undefined,
}),
);
const addons = useTechDocsAddons();
const { setShadowRoot } = useTechDocsReaderPage();
useEffect(() => {
const shadowHost = ref.current;
if (!dom || !shadowHost || shadowHost.shadowRoot) return;
setJss(
create({
...jssPreset(),
insertionPoint: dom.querySelector('head') || undefined,
}),
);
const shadowRoot = shadowHost.attachShadow({ mode: 'open' });
shadowRoot.innerHTML = '';
shadowRoot.appendChild(dom);
setShadowRoot(shadowRoot);
}, [dom, setShadowRoot]);
const contentElement = ref.current?.shadowRoot?.querySelector(
'[data-md-component="container"]',
);
const primarySidebarElement = ref.current?.shadowRoot?.querySelector(
'div[data-md-component="sidebar"][data-md-type="navigation"], div[data-md-component="navigation"]',
);
const secondarySidebarElement = ref.current?.shadowRoot?.querySelector(
'div[data-md-component="sidebar"][data-md-type="toc"], div[data-md-component="toc"]',
);
const primarySidebarAddonLocation = document.createElement('div');
primarySidebarElement?.prepend(primarySidebarAddonLocation);
const secondarySidebarAddonLocation = document.createElement('div');
secondarySidebarElement?.prepend(secondarySidebarAddonLocation);
// do not return content until dom is ready
if (!dom) {
return (
<Content>
<Progress />
</Content>
);
}
return (
<Content>
{/* sheetsManager={new Map()} is needed in order to deduplicate the injection of CSS in the page. */}
<StylesProvider jss={jss} sheetsManager={new Map()}>
<div ref={ref} data-testid="techdocs-native-shadowroot" />
<Portal container={primarySidebarAddonLocation}>
{addons.renderComponentsByLocation(locations.PRIMARY_SIDEBAR)}
</Portal>
<Portal container={contentElement}>
{addons.renderComponentsByLocation(locations.CONTENT)}
</Portal>
<Portal container={secondarySidebarAddonLocation}>
{addons.renderComponentsByLocation(locations.SECONDARY_SIDEBAR)}
</Portal>
</StylesProvider>
</Content>
);
};
@@ -1,17 +0,0 @@
/*
* 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.
*/
export { TechDocsReaderPageContent } from './TechDocsReaderPageContent';
@@ -1,61 +0,0 @@
/*
* 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 { Header } from '@backstage/core-components';
import { configApiRef, useApi } from '@backstage/core-plugin-api';
// todo(backstage/techdocs-core): Export these from @backstage/plugin-techdocs
import { Skeleton } from '@material-ui/lab';
import React, { useEffect } from 'react';
import Helmet from 'react-helmet';
import { useTechDocsAddons } from '../../addons';
import { useTechDocsMetadata, useTechDocsReaderPage } from '../../context';
import { TechDocsAddonLocations as locations } from '../../types';
const skeleton = <Skeleton animation="wave" variant="text" height={40} />;
export const TechDocsReaderPageHeader = () => {
const addons = useTechDocsAddons();
const configApi = useApi(configApiRef);
const { value: metadata } = useTechDocsMetadata();
const { title, setTitle, subtitle, setSubtitle } = useTechDocsReaderPage();
useEffect(() => {
if (!metadata) return;
setTitle(prevTitle => prevTitle || metadata.site_name);
setSubtitle(
prevSubtitle => prevSubtitle || metadata.site_description || 'Home',
);
}, [metadata, setTitle, setSubtitle]);
const appTitle = configApi.getOptional('app.title') || 'Backstage';
const tabTitle = [subtitle, title, appTitle].filter(Boolean).join(' | ');
return (
<Header
type="Documentation"
title={title || skeleton}
subtitle={subtitle || skeleton}
>
<Helmet titleTemplate="%s">
<title>{tabTitle}</title>
</Helmet>
{addons.renderComponentsByLocation(locations.HEADER)}
</Header>
);
};
@@ -1,17 +0,0 @@
/*
* 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.
*/
export { TechDocsReaderPageHeader } from './TechDocsReaderPageHeader';
@@ -1,49 +0,0 @@
/*
* 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 { Box, Toolbar, ToolbarProps, withStyles } from '@material-ui/core';
import React from 'react';
import { useTechDocsAddons } from '../../addons';
import { TechDocsAddonLocations as locations } from '../../types';
export const TechDocsReaderPageSubheader = withStyles(theme => ({
root: {
gridArea: 'pageSubheader',
flexDirection: 'column',
minHeight: 'auto',
padding: theme.spacing(3, 3, 0),
},
}))(({ ...rest }: ToolbarProps) => {
const addons = useTechDocsAddons();
if (!addons.renderComponentsByLocation(locations.SUBHEADER)) return null;
return (
<Toolbar {...rest}>
{addons.renderComponentsByLocation(locations.SUBHEADER) && (
<Box
display="flex"
justifyContent="flex-end"
width="100%"
flexWrap="wrap"
>
{addons.renderComponentsByLocation(locations.SUBHEADER)}
</Box>
)}
</Toolbar>
);
});
@@ -1,17 +0,0 @@
/*
* 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.
*/
export { TechDocsReaderPageSubheader } from './TechDocsReaderPageSubheader';
@@ -1,17 +0,0 @@
/*
* 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.
*/
export * from './TechDocsReaderPage';
@@ -1,146 +0,0 @@
/*
* 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 from 'react';
import { TechDocsMetadata } from './types';
import {
useEntityMetadata,
useTechDocsMetadata,
useTechDocsReaderPage,
TechDocsEntityProvider,
TechDocsMetadataProvider,
TechDocsReaderPageProvider,
} from './context';
import { renderHook, act } from '@testing-library/react-hooks';
import { Entity, CompoundEntityRef } from '@backstage/catalog-model';
const mockEntity: Entity = {
apiVersion: 'v1',
kind: 'Component',
metadata: { name: 'test-component', namespace: 'default' },
};
const mockTechDocsMetadata: TechDocsMetadata = {
site_name: 'test-componnet',
site_description: 'this is a test component',
};
const mockShadowRoot = () => {
const div = document.createElement('div');
const shadowRoot = div.attachShadow({ mode: 'open' });
shadowRoot.innerHTML = '<h1>Shadow DOM Mock</h1>';
return shadowRoot;
};
const wrapper = ({
entityName = {
namespace: mockEntity.metadata.namespace!!,
kind: mockEntity.kind,
name: mockEntity.metadata.name,
},
children,
}: {
entityName: CompoundEntityRef;
children: React.ReactNode;
}) => (
<TechDocsMetadataProvider
asyncValue={{
loading: false,
error: undefined,
value: mockTechDocsMetadata,
}}
>
<TechDocsEntityProvider
asyncValue={{
loading: false,
error: undefined,
value: mockEntity,
}}
>
<TechDocsReaderPageProvider entityName={entityName}>
{children}
</TechDocsReaderPageProvider>
</TechDocsEntityProvider>
</TechDocsMetadataProvider>
);
describe('context', () => {
describe('useEntityMetadata', () => {
it('should return loading state', async () => {
const { result } = renderHook(() => useEntityMetadata());
await expect(result.current.loading).toEqual(true);
});
it('should return expected entity values', async () => {
const { result } = renderHook(() => useEntityMetadata(), { wrapper });
expect(result.current.value).toBeDefined();
expect(result.current.error).toBeUndefined();
expect(result.current.value).toMatchObject(mockEntity);
});
});
describe('useTechDocsMetadata', () => {
it('should return loading state', async () => {
const { result } = renderHook(() => useTechDocsMetadata());
await expect(result.current.loading).toEqual(true);
});
it('should return expected techdocs metadata values', async () => {
const { result } = renderHook(() => useTechDocsMetadata(), { wrapper });
expect(result.current.value).toBeDefined();
expect(result.current.error).toBeUndefined();
expect(result.current.value).toMatchObject(mockTechDocsMetadata);
});
});
describe('useTechDocsReaderPage', () => {
it('should set title', () => {
const { result } = renderHook(() => useTechDocsReaderPage(), { wrapper });
expect(result.current.title).toBe('');
act(() => result.current.setTitle('test site title'));
expect(result.current.title).toBe('test site title');
});
it('should set subtitle', () => {
const { result } = renderHook(() => useTechDocsReaderPage(), { wrapper });
expect(result.current.subtitle).toBe('');
act(() => result.current.setSubtitle('test site subtitle'));
expect(result.current.subtitle).toBe('test site subtitle');
});
it('should set shadow root', async () => {
const { result } = renderHook(() => useTechDocsReaderPage(), { wrapper });
// mock shadowroot
const shadowRoot = mockShadowRoot();
act(() => result.current.setShadowRoot(shadowRoot));
expect(result.current.shadowRoot?.innerHTML).toBe(
'<h1>Shadow DOM Mock</h1>',
);
});
});
});
-148
View File
@@ -1,148 +0,0 @@
/*
* 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 { CompoundEntityRef } from '@backstage/catalog-model';
import React, {
createContext,
Dispatch,
PropsWithChildren,
SetStateAction,
useContext,
useState,
} from 'react';
import { AsyncState } from 'react-use/lib/useAsync';
import {
TechDocsAddonAsyncMetadata,
TechDocsEntityMetadata,
TechDocsMetadata,
} from './types';
type PropsWithAsyncMetadata<TMetadata> = PropsWithChildren<{
asyncValue: AsyncState<TMetadata>;
}>;
type PropsWithEntityName = PropsWithChildren<{ entityName: CompoundEntityRef }>;
const initialContextValue = {
loading: true,
error: undefined,
value: undefined,
};
const TechDocsMetadataContext =
createContext<TechDocsAddonAsyncMetadata<TechDocsMetadata>>(
initialContextValue,
);
export const TechDocsMetadataProvider = ({
asyncValue,
children,
}: PropsWithAsyncMetadata<TechDocsMetadata>) => {
return (
<TechDocsMetadataContext.Provider value={asyncValue}>
{children}
</TechDocsMetadataContext.Provider>
);
};
/**
* Hook for use within TechDocs addons to retrieve TechDocs Metadata for the
* current TechDocs site.
* @public
*/
export const useTechDocsMetadata = () => {
return useContext(TechDocsMetadataContext);
};
const TechDocsEntityContext =
createContext<TechDocsAddonAsyncMetadata<TechDocsEntityMetadata>>(
initialContextValue,
);
export const TechDocsEntityProvider = ({
asyncValue,
children,
}: PropsWithAsyncMetadata<TechDocsEntityMetadata>) => {
return (
<TechDocsEntityContext.Provider value={asyncValue}>
{children}
</TechDocsEntityContext.Provider>
);
};
/**
* Hook for use within TechDocs addons to retrieve Entity Metadata for the
* current TechDocs site.
* @public
*/
export const useEntityMetadata = () => {
return useContext(TechDocsEntityContext);
};
export type TechDocsReaderPageValue = {
entityName: CompoundEntityRef;
shadowRoot?: ShadowRoot;
setShadowRoot: Dispatch<SetStateAction<ShadowRoot | undefined>>;
title: string;
setTitle: Dispatch<SetStateAction<string>>;
subtitle: string;
setSubtitle: Dispatch<SetStateAction<string>>;
};
export const defaultTechDocsReaderPageValue: TechDocsReaderPageValue = {
title: '',
setTitle: () => {},
subtitle: '',
setSubtitle: () => {},
setShadowRoot: () => {},
entityName: { kind: '', name: '', namespace: '' },
};
export const TechDocsReaderPageContext = createContext<TechDocsReaderPageValue>(
defaultTechDocsReaderPageValue,
);
export const useTechDocsReaderPage = () => {
return useContext(TechDocsReaderPageContext);
};
export const TechDocsReaderPageProvider = ({
entityName,
children,
}: PropsWithEntityName) => {
const [title, setTitle] = useState(defaultTechDocsReaderPageValue.title);
const [subtitle, setSubtitle] = useState(
defaultTechDocsReaderPageValue.subtitle,
);
const [shadowRoot, setShadowRoot] = useState<ShadowRoot | undefined>(
defaultTechDocsReaderPageValue.shadowRoot,
);
const value = {
entityName,
shadowRoot,
setShadowRoot,
title,
setTitle,
subtitle,
setSubtitle,
};
return (
<TechDocsReaderPageContext.Provider value={value}>
{children}
</TechDocsReaderPageContext.Provider>
);
};
-51
View File
@@ -1,51 +0,0 @@
/*
* 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 { useShadowRoot, useShadowRootElements } from './hooks';
import { renderHook } from '@testing-library/react-hooks';
const mockShadowRoot = () => {
const div = document.createElement('div');
const shadowRoot = div.attachShadow({ mode: 'open' });
shadowRoot.innerHTML = '<h1>Shadow DOM Mock</h1>';
return shadowRoot;
};
const shadowRoot = mockShadowRoot();
jest.mock('./context', () => {
return {
useTechDocsReaderPage: () => ({ shadowRoot }),
};
});
describe('hooks', () => {
describe('useShadowRoot', () => {
it('should return shadow root', async () => {
const { result } = renderHook(() => useShadowRoot());
expect(result.current?.innerHTML).toBe(shadowRoot.innerHTML);
});
});
describe('useShadowRootElements', () => {
it('should return shadow root elements based on selector', () => {
const { result } = renderHook(() => useShadowRootElements(['h1']));
expect(result.current).toHaveLength(1);
});
});
});
-51
View File
@@ -1,51 +0,0 @@
/*
* 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 { useTechDocsReaderPage } from './context';
/**
* Hook for use within TechDocs addons that provides access to the underlying
* shadow root of the current page, allowing the DOM within to be mutated.
* @public
*/
export const useShadowRoot = () => {
const { shadowRoot } = useTechDocsReaderPage();
return shadowRoot;
};
/**
* Convenience hook for use within TechDocs addons that provides access to
* elements that match a given selector within the shadow root.
*
* todo(backstage/techdocs-core): Consider extending `selectors` from string[]
* to some kind of typed object array, so users have more control over the
* shape of the result. e.g. a flag to indicate querySelector vs.
* querySelectorAll.
*
* @public
*/
export const useShadowRootElements = <
TReturnedElement extends HTMLElement = HTMLElement,
>(
selectors: string[],
): TReturnedElement[] => {
const shadowRoot = useShadowRoot();
if (!shadowRoot) return [];
return selectors
.map(selector => shadowRoot?.querySelectorAll<TReturnedElement>(selector))
.filter(nodeList => nodeList.length)
.map(nodeList => Array.from(nodeList))
.flat();
};
-37
View File
@@ -1,37 +0,0 @@
/*
* 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 {
createTechDocsAddon,
TechDocsAddons,
TECHDOCS_ADDONS_WRAPPER_KEY,
} from './addons';
export * from './components';
export { useEntityMetadata, useTechDocsMetadata } from './context';
export { useShadowRoot, useShadowRootElements } from './hooks';
export { TechDocsAddonLocations } from './types';
export type {
TechDocsAddonAsyncMetadata,
TechDocsAddonOptions,
TechDocsMetadata,
TechDocsEntityMetadata,
} from './types';
@@ -1,17 +0,0 @@
/*
* 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.
*/
export * from './test-utils';
@@ -1,29 +0,0 @@
/*
* 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.
*/
export const useTechDocsReaderDom = jest.fn();
export const useParams = jest.fn();
jest.mock('@backstage/plugin-techdocs', () => ({
...(jest.requireActual('@backstage/plugin-techdocs') as {}),
useTechDocsReaderDom,
withTechDocsReaderProvider: jest.fn(x => x),
TechDocsStateIndicator: jest.fn(() => null),
}));
// todo(backstage/techdocs-core): Use core test-utils' `routeEntries` option.
jest.mock('react-router', () => ({
...(jest.requireActual('react-router') as {}),
useParams,
}));
@@ -1,219 +0,0 @@
/*
* 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 order matters for jest manual mocks! import this first.
import { useTechDocsReaderDom, useParams } from './mocks';
import React, { ReactElement, Fragment } from 'react';
// Shadow DOM support for the simple and complete DOM testing utilities
// https://github.com/testing-library/dom-testing-library/issues/742#issuecomment-674987855
import { screen } from 'testing-library__dom';
import { renderToStaticMarkup } from 'react-dom/server';
import { Route, Routes } from 'react-router-dom';
import { act, render } from '@testing-library/react';
import { AsyncState } from 'react-use/lib/useAsyncFn';
import {
wrapInTestApp,
TestApiProvider,
TestApiProviderProps,
} from '@backstage/test-utils';
import { TechDocsEntityMetadata, TechDocsMetadata } from '../types';
import { TechDocsReaderPage, TechDocsAddons } from '..';
type RecursivePartial<T> = {
[P in keyof T]?: RecursivePartial<T[P]>;
};
type Apis = TestApiProviderProps<any>['apis'];
export type TechDocsAddonsBuilder = {
dom: ReactElement;
entity: RecursivePartial<TechDocsEntityMetadata>;
metadata: RecursivePartial<TechDocsMetadata>;
componentId: string;
apis: Apis;
path: string;
};
const defaultOptions: TechDocsAddonsBuilder = {
dom: <></>,
entity: {},
metadata: {},
componentId: 'docs',
apis: [],
path: '',
};
const defaultMetadata = {
site_name: 'Tech Docs',
site_description: 'Tech Docs',
};
const defaultEntity = {
kind: 'Component',
metadata: { namespace: 'default', name: 'docs' },
};
const defaultDom = (
<html lang="en">
<head />
<body>
<div data-md-component="container">
<div data-md-component="navigation" />
<div data-md-component="toc" />
<div data-md-component="main" />
</div>
</body>
</html>
);
export class TechDocsAddonBuilder {
private options: TechDocsAddonsBuilder = defaultOptions;
private addons: ReactElement[];
static buildAddonsInTechDocs(addons: ReactElement[]) {
return new TechDocsAddonBuilder(addons);
}
constructor(addons: ReactElement[]) {
this.addons = addons;
}
withApis(apis: Apis) {
const refs = apis.map(([ref]) => ref);
this.options.apis = this.options.apis
.filter(([ref]) => !refs.includes(ref))
.concat(apis);
return this;
}
withDom(dom: ReactElement) {
this.options.dom = dom;
return this;
}
withMetadata(metadata: RecursivePartial<TechDocsMetadata>) {
this.options.metadata = metadata;
return this;
}
withEntity(entity: RecursivePartial<TechDocsEntityMetadata>) {
this.options.entity = entity;
return this;
}
atPath(path: string) {
this.options.path = path;
return this;
}
build() {
const apis = [...this.options.apis];
const entityName = {
namespace:
this.options.entity?.metadata?.namespace ||
defaultEntity.metadata.namespace,
kind: this.options.entity?.kind || defaultEntity.kind,
name: this.options.entity?.metadata?.name || defaultEntity.metadata.name,
};
const techDocsMetadata: AsyncState<TechDocsMetadata> = {
loading: false,
error: undefined,
value: (this.options.metadata || {
...defaultMetadata,
}) as TechDocsMetadata,
};
const entityMetadata: AsyncState<TechDocsEntityMetadata> = {
loading: false,
error: undefined,
value: (this.options.entity || {
...defaultEntity,
}) as TechDocsEntityMetadata,
};
const dom = document.createElement('html');
dom.innerHTML = renderToStaticMarkup(this.options.dom || defaultDom);
useTechDocsReaderDom.mockReturnValue(dom);
// todo(backstage/techdocs-core): Use core test-utils' `routeEntries` option to mock
// the current path. We use jest mocks instead for now because of a bug in
// react-router that prevents '*' params from being mocked.
useParams.mockReturnValue({
...entityName,
'*': this.options.path,
});
return wrapInTestApp(
<TestApiProvider apis={apis}>
<Routes>
<Route
path="*"
element={
<TechDocsReaderPage
dom={dom}
asyncEntityMetadata={entityMetadata}
asyncTechDocsMetadata={techDocsMetadata}
/>
}
>
<TechDocsAddons>
{this.addons.map((addon, index) => (
<Fragment key={index}>{addon}</Fragment>
))}
</TechDocsAddons>
</Route>
</Routes>
</TestApiProvider>,
);
}
render(): typeof screen & { shadowRoot: ShadowRoot | null } {
render(this.build());
const shadowHost = screen.getByTestId('techdocs-native-shadowroot');
return {
...screen,
shadowRoot: shadowHost?.shadowRoot,
};
}
// Components using useEffect to perform an asynchronous action (such as fetch) must be rendered within an async
// act call to properly get the final state, even with mocked responses. This utility method makes the signature a bit
// cleaner, since act doesn't return the result of the evaluated function.
// https://github.com/testing-library/react-testing-library/issues/281
// https://github.com/facebook/react/pull/14853
async renderWithEffects(): Promise<
ReturnType<TechDocsAddonBuilder['render']>
> {
await act(async () => {
this.render();
});
const shadowHost = screen.getByTestId('techdocs-native-shadowroot');
return {
...screen,
shadowRoot: shadowHost?.shadowRoot,
};
}
}
export default TechDocsAddonBuilder.buildAddonsInTechDocs;
-99
View File
@@ -1,99 +0,0 @@
/*
* 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<TAddonProps = {}> = {
name: string;
location: TechDocsAddonLocations;
component: ComponentType<TAddonProps>;
};
/**
* Common response envelope for addon-related hooks.
* @public
*/
export type TechDocsAddonAsyncMetadata<TValue> = AsyncState<TValue | undefined>;
/**
* Metadata for TechDocs page
*
* @public
*/
export type TechDocsMetadata = {
site_name: string;
site_description: string;
};
/**
* Metadata for TechDocs Entity
*
* @public
*/
export type TechDocsEntityMetadata = Entity & {
locationMetadata?: { type: string; target: string };
};