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:
@@ -1 +0,0 @@
|
||||
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
|
||||
@@ -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>;
|
||||
```
|
||||
@@ -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>;
|
||||
```
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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';
|
||||
-98
@@ -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';
|
||||
-61
@@ -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';
|
||||
-49
@@ -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>',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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 };
|
||||
};
|
||||
Reference in New Issue
Block a user