Merge pull request #10178 from backstage/techdocs/addon-integration
[TechDocs] Addon Framework
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/integration': patch
|
||||
---
|
||||
|
||||
Exported `replaceGitLabUrlType` from package
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
'@backstage/plugin-techdocs-react': minor
|
||||
---
|
||||
|
||||
This package will house frontend utilities related to TechDocs to be shared across other frontend Backstage packages.
|
||||
|
||||
In this release, it introduces a framework that can be used create TechDocs addons.
|
||||
|
||||
Note: this package is not necessarily stable yet. After iteration on this package, its stability will be signaled by a major-version bump.
|
||||
@@ -0,0 +1,56 @@
|
||||
---
|
||||
'@backstage/plugin-techdocs': minor
|
||||
---
|
||||
|
||||
TechDocs supports a new, experimental method of customization: addons!
|
||||
|
||||
To customize the standalone TechDocs reader page experience, update your `/packages/app/src/App.tsx` in the following way:
|
||||
|
||||
```diff
|
||||
import { TechDocsIndexPage, TechDocsReaderPage } from '@backstage/plugin-techdocs';
|
||||
+ import { TechDocsAddons } from '@backstage/plugin-techdocs-react';
|
||||
+ import { SomeAddon } from '@backstage/plugin-some-plugin';
|
||||
|
||||
// ...
|
||||
|
||||
<Route path="/docs" element={<TechDocsIndexPage />} />
|
||||
<Route
|
||||
path="/docs/:namespace/:kind/:name/*"
|
||||
element={<TechDocsReaderPage />}
|
||||
>
|
||||
+ <TechDocsAddons>
|
||||
+ <SomeAddon />
|
||||
+ </TechDocsAddons>
|
||||
</Route>
|
||||
|
||||
// ...
|
||||
```
|
||||
|
||||
To customize the TechDocs reader experience on the Catalog entity page, update your `packages/app/src/components/catalog/EntityPage.tsx` in the following way:
|
||||
|
||||
```diff
|
||||
import { EntityTechdocsContent } from '@backstage/plugin-techdocs';
|
||||
+ import { TechDocsAddons } from '@backstage/plugin-techdocs-react';
|
||||
+ import { SomeAddon } from '@backstage/plugin-some-plugin';
|
||||
|
||||
// ...
|
||||
|
||||
<EntityLayoutWrapper>
|
||||
<EntityLayout.Route path="/" title="Overview">
|
||||
{overviewContent}
|
||||
</EntityLayout.Route>
|
||||
|
||||
<EntityLayout.Route path="/docs" title="Docs">
|
||||
- <EntityTechDocsContent />
|
||||
+ <EntityTechdocsContent>
|
||||
+ <TechDocsAddons>
|
||||
+ <SomeAddon />
|
||||
+ </TechDocsAddons>
|
||||
+ </EntityTechdocsContent>
|
||||
</EntityLayout.Route>
|
||||
</EntityLayoutWrapper>
|
||||
|
||||
// ...
|
||||
```
|
||||
|
||||
If you do not wish to customize your TechDocs reader experience in this way at this time, no changes are necessary!
|
||||
@@ -1,5 +1,8 @@
|
||||
site_name: e2e Fixture Documentation
|
||||
site_description: Documentation used for end-to-end tests of TechDocs in Backstage.
|
||||
repo_url: https://github.com/backstage/backstage
|
||||
edit_uri: edit/master/cypress/fixtures/docs
|
||||
|
||||
nav:
|
||||
- Home: index.md
|
||||
- Sub-page 1: sub-page-one.md
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
site_name: 'Backstage'
|
||||
site_description: 'Main documentation for Backstage features and platform APIs'
|
||||
repo_url: https://github.com/backstage/backstage
|
||||
edit_uri: edit/master/docs
|
||||
|
||||
plugins:
|
||||
- techdocs-core
|
||||
|
||||
@@ -66,8 +66,8 @@ import { SearchPage } from '@backstage/plugin-search';
|
||||
import { TechRadarPage } from '@backstage/plugin-tech-radar';
|
||||
import {
|
||||
TechDocsIndexPage,
|
||||
techdocsPlugin,
|
||||
TechDocsReaderPage,
|
||||
techdocsPlugin,
|
||||
} from '@backstage/plugin-techdocs';
|
||||
import {
|
||||
UserSettingsPage,
|
||||
@@ -87,6 +87,7 @@ import { defaultPreviewTemplate } from './components/scaffolder/defaultPreviewTe
|
||||
import { searchPage } from './components/search/SearchPage';
|
||||
import { providers } from './identityProviders';
|
||||
import * as plugins from './plugins';
|
||||
|
||||
import { techDocsPage } from './components/techdocs/TechDocsPage';
|
||||
import { ApacheAirflowPage } from '@backstage/plugin-apache-airflow';
|
||||
import { PermissionedRoute } from '@backstage/plugin-permission-react';
|
||||
|
||||
@@ -14,29 +14,20 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Content } from '@backstage/core-components';
|
||||
import {
|
||||
TechDocsReaderPageHeader,
|
||||
TechDocsReaderPage,
|
||||
Reader,
|
||||
TechDocsReaderPageHeader,
|
||||
TechDocsReaderPageSubheader,
|
||||
TechDocsReaderPageContent,
|
||||
} from '@backstage/plugin-techdocs';
|
||||
import React from 'react';
|
||||
|
||||
const DefaultTechDocsPage = () => {
|
||||
return (
|
||||
<TechDocsReaderPage>
|
||||
{({ techdocsMetadataValue, entityMetadataValue, entityRef, onReady }) => (
|
||||
<>
|
||||
<TechDocsReaderPageHeader
|
||||
techDocsMetadata={techdocsMetadataValue}
|
||||
entityMetadata={entityMetadataValue}
|
||||
entityRef={entityRef}
|
||||
/>
|
||||
<Content data-testid="techdocs-content">
|
||||
<Reader onReady={onReady} entityRef={entityRef} />
|
||||
</Content>
|
||||
</>
|
||||
)}
|
||||
<TechDocsReaderPageHeader />
|
||||
<TechDocsReaderPageSubheader />
|
||||
<TechDocsReaderPageContent />
|
||||
</TechDocsReaderPage>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -416,6 +416,12 @@ export function replaceGitHubUrlType(
|
||||
type: 'blob' | 'tree' | 'edit',
|
||||
): string;
|
||||
|
||||
// @public
|
||||
export function replaceGitLabUrlType(
|
||||
url: string,
|
||||
type: 'blob' | 'tree' | 'edit',
|
||||
): string;
|
||||
|
||||
// @public
|
||||
export interface ScmIntegration {
|
||||
resolveEditUrl(url: string): string;
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { GitLabIntegration, replaceUrlType } from './GitLabIntegration';
|
||||
import { GitLabIntegration, replaceGitLabUrlType } from './GitLabIntegration';
|
||||
|
||||
describe('GitLabIntegration', () => {
|
||||
it('has a working factory', () => {
|
||||
@@ -55,28 +55,28 @@ describe('GitLabIntegration', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('replaceUrlType', () => {
|
||||
describe('replaceGitLabUrlType', () => {
|
||||
it('should replace with expected type', () => {
|
||||
expect(
|
||||
replaceUrlType(
|
||||
replaceGitLabUrlType(
|
||||
'https://gitlab.com/my-org/my-project/-/blob/develop/README.md',
|
||||
'edit',
|
||||
),
|
||||
).toBe('https://gitlab.com/my-org/my-project/-/edit/develop/README.md');
|
||||
expect(
|
||||
replaceUrlType(
|
||||
replaceGitLabUrlType(
|
||||
'https://gitlab.com/webmodules/blob/-/blob/develop/test',
|
||||
'tree',
|
||||
),
|
||||
).toBe('https://gitlab.com/webmodules/blob/-/tree/develop/test');
|
||||
expect(
|
||||
replaceUrlType(
|
||||
replaceGitLabUrlType(
|
||||
'https://gitlab.com/blob/blob/-/blob/develop/test',
|
||||
'tree',
|
||||
),
|
||||
).toBe('https://gitlab.com/blob/blob/-/tree/develop/test');
|
||||
expect(
|
||||
replaceUrlType(
|
||||
replaceGitLabUrlType(
|
||||
'https://gitlab.com/blob/blob/-/edit/develop/README.md',
|
||||
'tree',
|
||||
),
|
||||
|
||||
@@ -60,11 +60,18 @@ export class GitLabIntegration implements ScmIntegration {
|
||||
}
|
||||
|
||||
resolveEditUrl(url: string): string {
|
||||
return replaceUrlType(url, 'edit');
|
||||
return replaceGitLabUrlType(url, 'edit');
|
||||
}
|
||||
}
|
||||
|
||||
export function replaceUrlType(
|
||||
/**
|
||||
* Takes a GitLab URL and replaces the type part (blob, tree etc).
|
||||
*
|
||||
* @param url - The original URL
|
||||
* @param type - The desired type, e.g. 'blob', 'tree', 'edit'
|
||||
* @public
|
||||
*/
|
||||
export function replaceGitLabUrlType(
|
||||
url: string,
|
||||
type: 'blob' | 'tree' | 'edit',
|
||||
): string {
|
||||
|
||||
@@ -20,4 +20,4 @@ export {
|
||||
} from './config';
|
||||
export type { GitLabIntegrationConfig } from './config';
|
||||
export { getGitLabFileFetchUrl, getGitLabRequestOptions } from './core';
|
||||
export { GitLabIntegration } from './GitLabIntegration';
|
||||
export { GitLabIntegration, replaceGitLabUrlType } from './GitLabIntegration';
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"@backstage/integration-react": "^1.0.1-next.1",
|
||||
"@backstage/plugin-catalog": "^1.1.0-next.1",
|
||||
"@backstage/plugin-techdocs": "^1.0.1-next.1",
|
||||
"@backstage/plugin-techdocs-react": "^0.0.0",
|
||||
"@backstage/test-utils": "^1.0.1-next.1",
|
||||
"@backstage/theme": "^0.2.15",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
|
||||
@@ -16,20 +16,27 @@
|
||||
|
||||
import React from 'react';
|
||||
import { Navigate, Route } from 'react-router';
|
||||
import { createApp } from '@backstage/app-defaults';
|
||||
import { FlatRoutes } from '@backstage/core-app-api';
|
||||
import { CatalogEntityPage } from '@backstage/plugin-catalog';
|
||||
|
||||
import {
|
||||
DefaultTechDocsHome,
|
||||
TechDocsIndexPage,
|
||||
TechDocsReaderPage,
|
||||
techdocsPlugin,
|
||||
} from '@backstage/plugin-techdocs';
|
||||
import {
|
||||
createTechDocsAddonExtension,
|
||||
TechDocsAddons,
|
||||
TechDocsAddonLocations,
|
||||
} from '@backstage/plugin-techdocs-react';
|
||||
import { createApp } from '@backstage/app-defaults';
|
||||
import { FlatRoutes } from '@backstage/core-app-api';
|
||||
import { CatalogEntityPage } from '@backstage/plugin-catalog';
|
||||
|
||||
import { apis } from './apis';
|
||||
import { Root } from './components/Root';
|
||||
import { techDocsPage } from './components/TechDocsPage';
|
||||
import * as plugins from './plugins';
|
||||
import { configLoader } from './config';
|
||||
import { Root } from './components/Root';
|
||||
import { techDocsPage, TechDocsThemeToggle } from './components/TechDocsPage';
|
||||
|
||||
const app = createApp({
|
||||
apis,
|
||||
@@ -40,6 +47,14 @@ const app = createApp({
|
||||
const AppProvider = app.getProvider();
|
||||
const AppRouter = app.getRouter();
|
||||
|
||||
const ThemeToggleAddon = techdocsPlugin.provide(
|
||||
createTechDocsAddonExtension({
|
||||
name: 'ThemeToggleAddon',
|
||||
component: TechDocsThemeToggle,
|
||||
location: TechDocsAddonLocations.Header,
|
||||
}),
|
||||
);
|
||||
|
||||
const routes = (
|
||||
<FlatRoutes>
|
||||
<Navigate key="/" to="/docs/default/component/local/" />
|
||||
@@ -56,6 +71,9 @@ const routes = (
|
||||
element={<TechDocsReaderPage />}
|
||||
>
|
||||
{techDocsPage}
|
||||
<TechDocsAddons>
|
||||
<ThemeToggleAddon />
|
||||
</TechDocsAddons>
|
||||
</Route>
|
||||
</FlatRoutes>
|
||||
);
|
||||
|
||||
@@ -14,29 +14,20 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {
|
||||
FC,
|
||||
createContext,
|
||||
useContext,
|
||||
useState,
|
||||
useCallback,
|
||||
} from 'react';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { Theme, makeStyles } from '@material-ui/core';
|
||||
|
||||
import { ThemeProvider, Box, Tooltip, IconButton } from '@material-ui/core';
|
||||
import { Box, Tooltip, IconButton } from '@material-ui/core';
|
||||
import LightIcon from '@material-ui/icons/Brightness7';
|
||||
import DarkIcon from '@material-ui/icons/Brightness4';
|
||||
|
||||
import { lightTheme, darkTheme } from '@backstage/theme';
|
||||
import { CompoundEntityRef } from '@backstage/catalog-model';
|
||||
|
||||
import { Content } from '@backstage/core-components';
|
||||
import { appThemeApiRef, useApi } from '@backstage/core-plugin-api';
|
||||
|
||||
import {
|
||||
Reader,
|
||||
TechDocsReaderPage,
|
||||
TechDocsReaderPageHeader,
|
||||
TechDocsReaderPageContent,
|
||||
} from '@backstage/plugin-techdocs';
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => ({
|
||||
@@ -60,44 +51,12 @@ enum Themes {
|
||||
DARK = 'dark',
|
||||
}
|
||||
|
||||
type TechDocsThemeValue = {
|
||||
theme: Themes;
|
||||
toggleTheme: () => void;
|
||||
};
|
||||
|
||||
const TechDocsThemeContext = createContext<TechDocsThemeValue>({
|
||||
theme: Themes.LIGHT,
|
||||
toggleTheme: () => {},
|
||||
});
|
||||
|
||||
const TechdocsThemeProvider: FC = ({ children }) => {
|
||||
const [theme, setTheme] = useState<Themes>(Themes.LIGHT);
|
||||
|
||||
const toggleTheme = useCallback(() => {
|
||||
setTheme(prevTheme =>
|
||||
prevTheme === Themes.LIGHT ? Themes.DARK : Themes.LIGHT,
|
||||
);
|
||||
}, [setTheme]);
|
||||
|
||||
const value = { theme, toggleTheme };
|
||||
|
||||
const themes = {
|
||||
[Themes.LIGHT]: lightTheme,
|
||||
[Themes.DARK]: darkTheme,
|
||||
};
|
||||
|
||||
return (
|
||||
<TechDocsThemeContext.Provider value={value}>
|
||||
<ThemeProvider theme={themes[theme]}>{children}</ThemeProvider>
|
||||
</TechDocsThemeContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const useTechDocsTheme = () => useContext(TechDocsThemeContext);
|
||||
|
||||
const TechDocsThemeToggle = () => {
|
||||
export const TechDocsThemeToggle = () => {
|
||||
const appThemeApi = useApi(appThemeApiRef);
|
||||
const classes = useStyles();
|
||||
const { theme, toggleTheme } = useTechDocsTheme();
|
||||
const [theme, setTheme] = useState<Themes>(
|
||||
(appThemeApi.getActiveThemeId() as Themes) || Themes.LIGHT,
|
||||
);
|
||||
|
||||
const themes = {
|
||||
[Themes.LIGHT]: {
|
||||
@@ -112,10 +71,18 @@ const TechDocsThemeToggle = () => {
|
||||
|
||||
const { title, icon: Icon } = themes[theme];
|
||||
|
||||
const handleSetTheme = () => {
|
||||
setTheme(prevTheme => {
|
||||
const newTheme = prevTheme === Themes.LIGHT ? Themes.DARK : Themes.LIGHT;
|
||||
appThemeApi.setActiveThemeId(newTheme);
|
||||
return newTheme;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Box display="flex" alignItems="center" mr={2}>
|
||||
<Tooltip title={title} arrow>
|
||||
<IconButton size="small" onClick={toggleTheme}>
|
||||
<IconButton size="small" onClick={handleSetTheme}>
|
||||
<Icon className={classes.headerIcon} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
@@ -123,47 +90,13 @@ const TechDocsThemeToggle = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const TechDocsPageContent = ({
|
||||
onReady,
|
||||
entityRef,
|
||||
}: {
|
||||
entityRef: CompoundEntityRef;
|
||||
onReady: () => void;
|
||||
}) => {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<Content className={classes.content} data-testid="techdocs-content">
|
||||
<Reader onReady={onReady} entityRef={entityRef} withSearch={false} />
|
||||
</Content>
|
||||
);
|
||||
};
|
||||
|
||||
const DefaultTechDocsPage = () => {
|
||||
const techDocsMetadata = {
|
||||
site_name: 'Live preview environment',
|
||||
site_description: '',
|
||||
};
|
||||
|
||||
return (
|
||||
<TechDocsReaderPage>
|
||||
{({ entityRef, onReady }) => (
|
||||
<>
|
||||
<TechDocsReaderPageHeader
|
||||
entityRef={entityRef}
|
||||
techDocsMetadata={techDocsMetadata}
|
||||
>
|
||||
<TechDocsThemeToggle />
|
||||
</TechDocsReaderPageHeader>
|
||||
<TechDocsPageContent entityRef={entityRef} onReady={onReady} />
|
||||
</>
|
||||
)}
|
||||
<TechDocsReaderPageHeader />
|
||||
<TechDocsReaderPageContent withSearch={false} />
|
||||
</TechDocsReaderPage>
|
||||
);
|
||||
};
|
||||
|
||||
export const techDocsPage = (
|
||||
<TechdocsThemeProvider>
|
||||
<DefaultTechDocsPage />
|
||||
</TechdocsThemeProvider>
|
||||
);
|
||||
export const techDocsPage = <DefaultTechDocsPage />;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
site_name: 'Example Documentation'
|
||||
repo_url: https://github.com/backstage/backstage
|
||||
edit_uri: edit/master/plugins/techdocs-backend/examples/documented-component/docs
|
||||
|
||||
nav:
|
||||
- Home: index.md
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
|
||||
@@ -0,0 +1,9 @@
|
||||
# @backstage/plugin-techdocs-react
|
||||
|
||||
This package provides frontend utilities for TechDocs and Addons.
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
yarn add --cwd packages/app @backstage/plugin-techdocs-react
|
||||
```
|
||||
@@ -0,0 +1,129 @@
|
||||
## API Report File for "@backstage/plugin-techdocs-react"
|
||||
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import { ApiRef } from '@backstage/core-plugin-api';
|
||||
import { AsyncState } from 'react-use/lib/useAsync';
|
||||
import { ComponentType } from 'react';
|
||||
import { CompoundEntityRef } from '@backstage/catalog-model';
|
||||
import { Dispatch } from 'react';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { Extension } from '@backstage/core-plugin-api';
|
||||
import { default as React_2 } from 'react';
|
||||
import { ReactNode } from 'react';
|
||||
import { SetStateAction } from 'react';
|
||||
|
||||
// @alpha
|
||||
export function createTechDocsAddonExtension<TComponentProps>(
|
||||
options: TechDocsAddonOptions<TComponentProps>,
|
||||
): Extension<ComponentType<TComponentProps>>;
|
||||
|
||||
// @alpha (undocumented)
|
||||
export const defaultTechDocsReaderPageValue: TechDocsReaderPageValue;
|
||||
|
||||
// @alpha
|
||||
export const TECHDOCS_ADDONS_WRAPPER_KEY = 'techdocs.addons.wrapper.v1';
|
||||
|
||||
// @alpha
|
||||
export const TechDocsAddonLocations: Readonly<{
|
||||
readonly Header: 'Header';
|
||||
readonly Subheader: 'Subheader';
|
||||
readonly PrimarySidebar: 'PrimarySidebar';
|
||||
readonly SecondarySidebar: 'SecondarySidebar';
|
||||
readonly Content: 'Content';
|
||||
}>;
|
||||
|
||||
// @alpha
|
||||
export type TechDocsAddonOptions<TAddonProps = {}> = {
|
||||
name: string;
|
||||
location: keyof typeof TechDocsAddonLocations;
|
||||
component: ComponentType<TAddonProps>;
|
||||
};
|
||||
|
||||
// @alpha
|
||||
export const TechDocsAddons: React_2.ComponentType;
|
||||
|
||||
// @public
|
||||
export interface TechDocsApi {
|
||||
// (undocumented)
|
||||
getApiOrigin(): Promise<string>;
|
||||
// (undocumented)
|
||||
getEntityMetadata(
|
||||
entityId: CompoundEntityRef,
|
||||
): Promise<TechDocsEntityMetadata>;
|
||||
// (undocumented)
|
||||
getTechDocsMetadata(entityId: CompoundEntityRef): Promise<TechDocsMetadata>;
|
||||
}
|
||||
|
||||
// @public
|
||||
export const techdocsApiRef: ApiRef<TechDocsApi>;
|
||||
|
||||
// @public
|
||||
export type TechDocsEntityMetadata = Entity & {
|
||||
locationMetadata?: {
|
||||
type: string;
|
||||
target: string;
|
||||
};
|
||||
};
|
||||
|
||||
// @public
|
||||
export type TechDocsMetadata = {
|
||||
site_name: string;
|
||||
site_description: string;
|
||||
};
|
||||
|
||||
// @public
|
||||
export const TechDocsReaderPageProvider: React_2.MemoExoticComponent<
|
||||
({ entityRef, children }: TechDocsReaderPageProviderProps) => JSX.Element
|
||||
>;
|
||||
|
||||
// @public
|
||||
export type TechDocsReaderPageProviderProps = {
|
||||
entityRef: CompoundEntityRef;
|
||||
children: TechDocsReaderPageProviderRenderFunction | ReactNode;
|
||||
};
|
||||
|
||||
// @public
|
||||
export type TechDocsReaderPageProviderRenderFunction = (
|
||||
value: TechDocsReaderPageValue,
|
||||
) => JSX.Element;
|
||||
|
||||
// @public
|
||||
export type TechDocsReaderPageValue = {
|
||||
metadata: AsyncState<TechDocsMetadata>;
|
||||
entityRef: CompoundEntityRef;
|
||||
entityMetadata: AsyncState<TechDocsEntityMetadata>;
|
||||
shadowRoot?: ShadowRoot;
|
||||
setShadowRoot: Dispatch<SetStateAction<ShadowRoot | undefined>>;
|
||||
title: string;
|
||||
setTitle: Dispatch<SetStateAction<string>>;
|
||||
subtitle: string;
|
||||
setSubtitle: Dispatch<SetStateAction<string>>;
|
||||
onReady?: () => void;
|
||||
};
|
||||
|
||||
// @alpha
|
||||
export const useShadowRoot: () => ShadowRoot | undefined;
|
||||
|
||||
// @alpha
|
||||
export const useShadowRootElements: <
|
||||
TReturnedElement extends HTMLElement = HTMLElement,
|
||||
>(
|
||||
selectors: string[],
|
||||
) => TReturnedElement[];
|
||||
|
||||
// @alpha
|
||||
export const useShadowRootSelection: (wait?: number) => Selection | null;
|
||||
|
||||
// @alpha
|
||||
export const useTechDocsAddons: () => {
|
||||
renderComponentByName: (name: string) => JSX.Element | null;
|
||||
renderComponentsByLocation: (
|
||||
location: keyof typeof TechDocsAddonLocations,
|
||||
) => (JSX.Element | null)[] | null;
|
||||
};
|
||||
|
||||
// @alpha
|
||||
export const useTechDocsReaderPage: () => TechDocsReaderPageValue;
|
||||
```
|
||||
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"name": "@backstage/plugin-techdocs-react",
|
||||
"description": "Shared frontend utilities for TechDocs and Addons",
|
||||
"version": "0.0.0",
|
||||
"private": false,
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"alphaTypes": "dist/index.alpha.d.ts",
|
||||
"main": "dist/index.esm.js",
|
||||
"types": "dist/index.d.ts"
|
||||
},
|
||||
"backstage": {
|
||||
"role": "web-library"
|
||||
},
|
||||
"homepage": "https://backstage.io",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/backstage/backstage",
|
||||
"directory": "plugins/techdocs-react"
|
||||
},
|
||||
"keywords": [
|
||||
"backstage",
|
||||
"techdocs"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"scripts": {
|
||||
"build": "backstage-cli package build --experimental-type-build",
|
||||
"lint": "backstage-cli package lint",
|
||||
"test": "backstage-cli package test",
|
||||
"prepack": "backstage-cli package prepack",
|
||||
"postpack": "backstage-cli package postpack",
|
||||
"clean": "backstage-cli package clean",
|
||||
"start": "backstage-cli package start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/catalog-model": "^1.0.1-next.1",
|
||||
"@backstage/core-components": "^0.9.3-next.1",
|
||||
"@backstage/core-plugin-api": "^1.0.0",
|
||||
"@backstage/version-bridge": "^1.0.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": "^4.17.21",
|
||||
"react-helmet": "6.1.0",
|
||||
"react-router-dom": "6.0.0-beta.0",
|
||||
"react-use": "^17.2.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^16.13.1 || ^17.0.0",
|
||||
"react": "^16.13.1 || ^17.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/react": "^12.1.3",
|
||||
"@testing-library/react-hooks": "^7.0.2",
|
||||
"@backstage/test-utils": "^1.0.1-next.1",
|
||||
"@backstage/theme": "^0.2.15"
|
||||
},
|
||||
"files": [
|
||||
"alpha",
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { ComponentType, useCallback } from 'react';
|
||||
import { useOutlet } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
attachComponentData,
|
||||
createReactExtension,
|
||||
ElementCollection,
|
||||
Extension,
|
||||
useElementFilter,
|
||||
} from '@backstage/core-plugin-api';
|
||||
|
||||
import { TechDocsAddonLocations, TechDocsAddonOptions } from './types';
|
||||
|
||||
export const TECHDOCS_ADDONS_KEY = 'techdocs.addons.addon.v1';
|
||||
|
||||
/**
|
||||
* Marks the <TechDocsAddons> registry component.
|
||||
* @alpha
|
||||
*/
|
||||
export const TECHDOCS_ADDONS_WRAPPER_KEY = 'techdocs.addons.wrapper.v1';
|
||||
|
||||
/**
|
||||
* TechDocs Addon registry.
|
||||
* @alpha
|
||||
*/
|
||||
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.
|
||||
* @alpha
|
||||
*/
|
||||
export function createTechDocsAddonExtension<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,
|
||||
): JSX.Element | undefined => {
|
||||
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,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* hook to use addons in components
|
||||
* @alpha
|
||||
*/
|
||||
export const useTechDocsAddons = () => {
|
||||
const node = useOutlet();
|
||||
const collection = useElementFilter(node, getAllTechDocsAddons);
|
||||
const options = useElementFilter(node, getAllTechDocsAddonsData);
|
||||
|
||||
const findAddonByData = useCallback(
|
||||
(data: TechDocsAddonOptions | undefined) => {
|
||||
if (!collection || !data) return null;
|
||||
const nameKey = getDataKeyByName(data.name);
|
||||
return getTechDocsAddonByName(collection, nameKey) ?? null;
|
||||
},
|
||||
[collection],
|
||||
);
|
||||
|
||||
const renderComponentByName = useCallback(
|
||||
(name: string) => {
|
||||
const data = options.find(option => option.name === name);
|
||||
return data ? findAddonByData(data) : null;
|
||||
},
|
||||
[options, findAddonByData],
|
||||
);
|
||||
|
||||
const renderComponentsByLocation = useCallback(
|
||||
(location: keyof typeof TechDocsAddonLocations) => {
|
||||
const data = options.filter(option => option.location === location);
|
||||
return data.length ? data.map(findAddonByData) : null;
|
||||
},
|
||||
[options, findAddonByData],
|
||||
);
|
||||
|
||||
return { renderComponentByName, renderComponentsByLocation };
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* 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 { createApiRef } from '@backstage/core-plugin-api';
|
||||
import { TechDocsEntityMetadata, TechDocsMetadata } from './types';
|
||||
|
||||
/**
|
||||
* API to talk to techdocs-backend.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface TechDocsApi {
|
||||
getApiOrigin(): Promise<string>;
|
||||
getTechDocsMetadata(entityId: CompoundEntityRef): Promise<TechDocsMetadata>;
|
||||
getEntityMetadata(
|
||||
entityId: CompoundEntityRef,
|
||||
): Promise<TechDocsEntityMetadata>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility API reference for the {@link TechDocsApi}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const techdocsApiRef = createApiRef<TechDocsApi>({
|
||||
id: 'plugin.techdocs.service',
|
||||
});
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* 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 { renderHook, act } from '@testing-library/react-hooks';
|
||||
|
||||
import { ThemeProvider } from '@material-ui/core';
|
||||
|
||||
import { lightTheme } from '@backstage/theme';
|
||||
import { TestApiProvider } from '@backstage/test-utils';
|
||||
import { Entity, CompoundEntityRef } from '@backstage/catalog-model';
|
||||
|
||||
import { techdocsApiRef } from './api';
|
||||
import { useTechDocsReaderPage, TechDocsReaderPageProvider } from './context';
|
||||
import { TechDocsMetadata } from './types';
|
||||
|
||||
const mockShadowRoot = () => {
|
||||
const div = document.createElement('div');
|
||||
const shadowRoot = div.attachShadow({ mode: 'open' });
|
||||
shadowRoot.innerHTML = '<h1>Shadow DOM Mock</h1>';
|
||||
return shadowRoot;
|
||||
};
|
||||
|
||||
const mockEntityMetadata: Entity = {
|
||||
apiVersion: 'v1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'test',
|
||||
namespace: 'default',
|
||||
},
|
||||
spec: {
|
||||
owner: 'test',
|
||||
},
|
||||
};
|
||||
|
||||
const mockTechDocsMetadata: TechDocsMetadata = {
|
||||
site_name: 'test-componnet',
|
||||
site_description: 'this is a test component',
|
||||
};
|
||||
|
||||
const techdocsApiMock = {
|
||||
getEntityMetadata: jest.fn().mockResolvedValue(mockEntityMetadata),
|
||||
getTechDocsMetadata: jest.fn().mockResolvedValue(mockTechDocsMetadata),
|
||||
};
|
||||
|
||||
const wrapper = ({
|
||||
entityRef = {
|
||||
kind: mockEntityMetadata.kind,
|
||||
name: mockEntityMetadata.metadata.name,
|
||||
namespace: mockEntityMetadata.metadata.namespace!!,
|
||||
},
|
||||
children,
|
||||
}: {
|
||||
entityRef?: CompoundEntityRef;
|
||||
children: React.ReactNode;
|
||||
}) => (
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<TestApiProvider apis={[[techdocsApiRef, techdocsApiMock]]}>
|
||||
<TechDocsReaderPageProvider entityRef={entityRef}>
|
||||
{children}
|
||||
</TechDocsReaderPageProvider>
|
||||
</TestApiProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
describe('useTechDocsReaderPage', () => {
|
||||
it('should set title', async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(
|
||||
() => useTechDocsReaderPage(),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
expect(result.current.title).toBe('');
|
||||
|
||||
act(() => result.current.setTitle('test site title'));
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(result.current.title).toBe('test site title');
|
||||
});
|
||||
|
||||
it('should set subtitle', async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(
|
||||
() => useTechDocsReaderPage(),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
expect(result.current.subtitle).toBe('');
|
||||
|
||||
act(() => result.current.setSubtitle('test site subtitle'));
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(result.current.subtitle).toBe('test site subtitle');
|
||||
});
|
||||
|
||||
it('should set shadow root', async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(
|
||||
() => useTechDocsReaderPage(),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
// mock shadowroot
|
||||
const shadowRoot = mockShadowRoot();
|
||||
|
||||
act(() => result.current.setShadowRoot(shadowRoot));
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(result.current.shadowRoot?.innerHTML).toBe(
|
||||
'<h1>Shadow DOM Mock</h1>',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
* 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, {
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
useContext,
|
||||
useState,
|
||||
memo,
|
||||
ReactNode,
|
||||
} from 'react';
|
||||
import useAsync, { AsyncState } from 'react-use/lib/useAsync';
|
||||
|
||||
import {
|
||||
CompoundEntityRef,
|
||||
stringifyEntityRef,
|
||||
} from '@backstage/catalog-model';
|
||||
import {
|
||||
createVersionedContext,
|
||||
createVersionedValueMap,
|
||||
} from '@backstage/version-bridge';
|
||||
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
|
||||
import { techdocsApiRef } from './api';
|
||||
import { TechDocsEntityMetadata, TechDocsMetadata } from './types';
|
||||
|
||||
const areEntityRefsEqual = (
|
||||
prevEntityRef: CompoundEntityRef,
|
||||
nextEntityRef: CompoundEntityRef,
|
||||
) => {
|
||||
return (
|
||||
stringifyEntityRef(prevEntityRef) === stringifyEntityRef(nextEntityRef)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* @public type for the value of the TechDocsReaderPageContext
|
||||
*/
|
||||
export type TechDocsReaderPageValue = {
|
||||
metadata: AsyncState<TechDocsMetadata>;
|
||||
entityRef: CompoundEntityRef;
|
||||
entityMetadata: AsyncState<TechDocsEntityMetadata>;
|
||||
shadowRoot?: ShadowRoot;
|
||||
setShadowRoot: Dispatch<SetStateAction<ShadowRoot | undefined>>;
|
||||
title: string;
|
||||
setTitle: Dispatch<SetStateAction<string>>;
|
||||
subtitle: string;
|
||||
setSubtitle: Dispatch<SetStateAction<string>>;
|
||||
/**
|
||||
* @deprecated property can be passed down directly to the `TechDocsReaderPageContent` instead.
|
||||
*/
|
||||
onReady?: () => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
*/
|
||||
export const defaultTechDocsReaderPageValue: TechDocsReaderPageValue = {
|
||||
title: '',
|
||||
subtitle: '',
|
||||
setTitle: () => {},
|
||||
setSubtitle: () => {},
|
||||
setShadowRoot: () => {},
|
||||
metadata: { loading: true },
|
||||
entityMetadata: { loading: true },
|
||||
entityRef: { kind: '', name: '', namespace: '' },
|
||||
};
|
||||
|
||||
const TechDocsReaderPageContext = createVersionedContext<{
|
||||
1: TechDocsReaderPageValue;
|
||||
}>('techdocs-reader-page-context');
|
||||
|
||||
/**
|
||||
* render function for {@link TechDocsReaderPageProvider}
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type TechDocsReaderPageProviderRenderFunction = (
|
||||
value: TechDocsReaderPageValue,
|
||||
) => JSX.Element;
|
||||
|
||||
/**
|
||||
* Props for {@link TechDocsReaderPageProvider}
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type TechDocsReaderPageProviderProps = {
|
||||
entityRef: CompoundEntityRef;
|
||||
children: TechDocsReaderPageProviderRenderFunction | ReactNode;
|
||||
};
|
||||
|
||||
/**
|
||||
* A context to store the reader page state
|
||||
* @public
|
||||
*/
|
||||
export const TechDocsReaderPageProvider = memo(
|
||||
({ entityRef, children }: TechDocsReaderPageProviderProps) => {
|
||||
const techdocsApi = useApi(techdocsApiRef);
|
||||
|
||||
const metadata = useAsync(async () => {
|
||||
return techdocsApi.getTechDocsMetadata(entityRef);
|
||||
}, [entityRef]);
|
||||
|
||||
const entityMetadata = useAsync(async () => {
|
||||
return techdocsApi.getEntityMetadata(entityRef);
|
||||
}, [entityRef]);
|
||||
|
||||
const [title, setTitle] = useState(defaultTechDocsReaderPageValue.title);
|
||||
const [subtitle, setSubtitle] = useState(
|
||||
defaultTechDocsReaderPageValue.subtitle,
|
||||
);
|
||||
const [shadowRoot, setShadowRoot] = useState<ShadowRoot | undefined>(
|
||||
defaultTechDocsReaderPageValue.shadowRoot,
|
||||
);
|
||||
|
||||
const value = {
|
||||
metadata,
|
||||
entityRef,
|
||||
entityMetadata,
|
||||
shadowRoot,
|
||||
setShadowRoot,
|
||||
title,
|
||||
setTitle,
|
||||
subtitle,
|
||||
setSubtitle,
|
||||
};
|
||||
const versionedValue = createVersionedValueMap({ 1: value });
|
||||
|
||||
return (
|
||||
<TechDocsReaderPageContext.Provider value={versionedValue}>
|
||||
{children instanceof Function ? children(value) : children}
|
||||
</TechDocsReaderPageContext.Provider>
|
||||
);
|
||||
},
|
||||
(prevProps, nextProps) => {
|
||||
return areEntityRefsEqual(prevProps.entityRef, nextProps.entityRef);
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* Hook used to get access to shared state between reader page components.
|
||||
* @alpha
|
||||
*/
|
||||
export const useTechDocsReaderPage = () => {
|
||||
const versionedContext = useContext(TechDocsReaderPageContext);
|
||||
|
||||
if (versionedContext === undefined) {
|
||||
return defaultTechDocsReaderPageValue;
|
||||
}
|
||||
|
||||
const context = versionedContext.atVersion(1);
|
||||
if (context === undefined) {
|
||||
throw new Error('No context found for version 1.');
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* 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,
|
||||
useShadowRootSelection,
|
||||
} from './hooks';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { fireEvent, waitFor } from '@testing-library/react';
|
||||
|
||||
const fireSelectionChangeEvent = (window: Window) => {
|
||||
const selectionChangeEvent = window.document.createEvent('Event');
|
||||
selectionChangeEvent.initEvent('selectionchange', true, true);
|
||||
window.document.addEventListener('selectionchange', () => {}, false);
|
||||
fireEvent(window.document, selectionChangeEvent);
|
||||
};
|
||||
|
||||
const getSelection = jest.fn();
|
||||
|
||||
const mockShadowRoot = () => {
|
||||
const div = document.createElement('div');
|
||||
const shadowRoot = div.attachShadow({ mode: 'open' });
|
||||
shadowRoot.innerHTML = '<h1>Shadow DOM Mock</h1>';
|
||||
(shadowRoot as ShadowRoot & Pick<Document, 'getSelection'>).getSelection =
|
||||
getSelection;
|
||||
return shadowRoot;
|
||||
};
|
||||
|
||||
const shadowRoot = mockShadowRoot();
|
||||
|
||||
jest.mock('./context', () => {
|
||||
return {
|
||||
useTechDocsReaderPage: () => ({ shadowRoot }),
|
||||
};
|
||||
});
|
||||
|
||||
const selection = {
|
||||
type: 'Range',
|
||||
rangeCount: 1,
|
||||
isCollapsed: true,
|
||||
getRangeAt: () => ({
|
||||
startContainer: 'this is a sentence',
|
||||
endContainer: 'this is a sentence',
|
||||
startOffset: 1,
|
||||
endOffset: 3,
|
||||
getBoundingClientRect: () => ({
|
||||
right: 100,
|
||||
top: 100,
|
||||
width: 100,
|
||||
height: 100,
|
||||
}),
|
||||
}),
|
||||
toString: () => 'his ',
|
||||
containsNode: () => true,
|
||||
} as unknown as Selection;
|
||||
|
||||
getSelection.mockReturnValue(selection);
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useShadowRootSelection', () => {
|
||||
it('should return shadow root selection', async () => {
|
||||
const { result } = renderHook(() => useShadowRootSelection(0));
|
||||
|
||||
expect(result.current).toBeNull();
|
||||
|
||||
fireSelectionChangeEvent(window);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current?.toString()).toEqual('his ');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* 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 { useState, useEffect, useMemo } from 'react';
|
||||
import debounce from 'lodash/debounce';
|
||||
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.
|
||||
* @alpha
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @alpha
|
||||
*/
|
||||
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();
|
||||
};
|
||||
|
||||
const isValidSelection = (newSelection: Selection) => {
|
||||
// Safari sets the selection rect to top zero
|
||||
return (
|
||||
newSelection.toString() &&
|
||||
newSelection.rangeCount &&
|
||||
newSelection.getRangeAt(0).getBoundingClientRect().top
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook for retreiving a selection within the ShadowRoot.
|
||||
* @alpha
|
||||
*/
|
||||
export const useShadowRootSelection = (wait: number = 0) => {
|
||||
const shadowRoot = useShadowRoot();
|
||||
const [selection, setSelection] = useState<Selection | null>(null);
|
||||
const handleSelectionChange = useMemo(
|
||||
() =>
|
||||
debounce(() => {
|
||||
const shadowDocument = shadowRoot as ShadowRoot &
|
||||
Pick<Document, 'getSelection'>;
|
||||
// Firefox and Safari don't implement getSelection for Shadow DOM
|
||||
const newSelection = shadowDocument.getSelection
|
||||
? shadowDocument.getSelection()
|
||||
: document.getSelection();
|
||||
|
||||
if (newSelection && isValidSelection(newSelection)) {
|
||||
setSelection(newSelection);
|
||||
} else {
|
||||
setSelection(null);
|
||||
}
|
||||
}, wait),
|
||||
[shadowRoot, setSelection, wait],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
window.document.addEventListener('selectionchange', handleSelectionChange);
|
||||
return () =>
|
||||
window.document.removeEventListener(
|
||||
'selectionchange',
|
||||
handleSelectionChange,
|
||||
);
|
||||
}, [handleSelectionChange]);
|
||||
|
||||
return selection;
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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 utilities to be shared by frontend TechDocs plugins.
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
export {
|
||||
useTechDocsAddons,
|
||||
createTechDocsAddonExtension,
|
||||
TechDocsAddons,
|
||||
TECHDOCS_ADDONS_WRAPPER_KEY,
|
||||
} from './addons';
|
||||
export { techdocsApiRef } from './api';
|
||||
export type { TechDocsApi } from './api';
|
||||
export {
|
||||
defaultTechDocsReaderPageValue,
|
||||
TechDocsReaderPageProvider,
|
||||
useTechDocsReaderPage,
|
||||
} from './context';
|
||||
export type {
|
||||
TechDocsReaderPageProviderProps,
|
||||
TechDocsReaderPageProviderRenderFunction,
|
||||
TechDocsReaderPageValue,
|
||||
} from './context';
|
||||
export {
|
||||
useShadowRoot,
|
||||
useShadowRootElements,
|
||||
useShadowRootSelection,
|
||||
} from './hooks';
|
||||
export { TechDocsAddonLocations } from './types';
|
||||
export type {
|
||||
TechDocsEntityMetadata,
|
||||
TechDocsMetadata,
|
||||
TechDocsAddonOptions,
|
||||
} from './types';
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* 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 { ComponentType } from 'react';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
|
||||
/**
|
||||
* 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 };
|
||||
};
|
||||
|
||||
/**
|
||||
* Locations for which TechDocs addons may be declared and rendered.
|
||||
* @alpha
|
||||
*/
|
||||
export const TechDocsAddonLocations = Object.freeze({
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
PrimarySidebar: 'PrimarySidebar',
|
||||
|
||||
/**
|
||||
* These addons appear right of the content and above the table of contents.
|
||||
*/
|
||||
SecondarySidebar: 'SecondarySidebar',
|
||||
|
||||
/**
|
||||
* 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',
|
||||
|
||||
/**
|
||||
* todo(backstage/community): This is a proposed virtual location which would
|
||||
* help implement a common addon pattern in which many instances of a given
|
||||
* element in markdown would be dynamically replaced at render-time based on
|
||||
* attributes provided on that element, for example:
|
||||
*
|
||||
* ```md
|
||||
* ## For Fun
|
||||
* <TechDocsAddon>CatGif</TechDocsAddon>
|
||||
*
|
||||
* ## Component Metadata
|
||||
* <TechDocsAddon entityRef="default:component/some-component-name">CatalogEntityCard</TechDocsAddon>
|
||||
*
|
||||
* ## System Metadata
|
||||
* <TechDocsAddon entityRef="default:system/some-system-name">CatalogEntityCard</TechDocsAddon>
|
||||
* ```
|
||||
*
|
||||
* Could correspond to a TechDocs addon named `CatalogEntityCard` with
|
||||
* location `TechDocsAddonLocations.COMPONENT`, whose `component` would be
|
||||
* the react component that would be rendered in place of all instances of
|
||||
* the markdown illustrated above.
|
||||
*
|
||||
* The `@backstage/plugin-techdocs-react` package would need to be updated to, in
|
||||
* cases where such addons had been registered, find all instances of the
|
||||
* the `<TechDocsAddon>` tag whose `textContent` corresponded with the name of the
|
||||
* addon, then replace them with component instances of the addon component,
|
||||
* passing any attributes from the tag as props to the component.
|
||||
*/
|
||||
// Component: 'Component',
|
||||
} as const);
|
||||
|
||||
/**
|
||||
* Options for creating a TechDocs addon.
|
||||
* @alpha
|
||||
*/
|
||||
export type TechDocsAddonOptions<TAddonProps = {}> = {
|
||||
name: string;
|
||||
location: keyof typeof TechDocsAddonLocations;
|
||||
component: ComponentType<TAddonProps>;
|
||||
};
|
||||
+106
-35
@@ -16,11 +16,33 @@ import { FetchApi } from '@backstage/core-plugin-api';
|
||||
import { IdentityApi } from '@backstage/core-plugin-api';
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { default as React_2 } from 'react';
|
||||
import { ReactNode } from 'react';
|
||||
import { RouteRef } from '@backstage/core-plugin-api';
|
||||
import { StyledComponentProps } from '@material-ui/core';
|
||||
import { TableColumn } from '@backstage/core-components';
|
||||
import { TableProps } from '@backstage/core-components';
|
||||
import { TechDocsEntityMetadata as TechDocsEntityMetadata_2 } from '@backstage/plugin-techdocs-react';
|
||||
import { TechDocsMetadata as TechDocsMetadata_2 } from '@backstage/plugin-techdocs-react';
|
||||
import { ToolbarProps } from '@material-ui/core';
|
||||
import { UserListFilterKind } from '@backstage/plugin-catalog-react';
|
||||
|
||||
// @public
|
||||
export type ContentStateTypes =
|
||||
/** There is nothing to display but a loading indicator */
|
||||
| 'CHECKING'
|
||||
/** There is no content yet -> present a full screen loading page */
|
||||
| 'INITIAL_BUILD'
|
||||
/** There is content, but the backend is about to update it */
|
||||
| 'CONTENT_STALE_REFRESHING'
|
||||
/** There is content, but after a reload, the content will be different */
|
||||
| 'CONTENT_STALE_READY'
|
||||
/** There is content, the backend tried to update it, but failed */
|
||||
| 'CONTENT_STALE_ERROR'
|
||||
/** There is nothing to see but a "not found" page. Is also shown on page load errors */
|
||||
| 'CONTENT_NOT_FOUND'
|
||||
/** There is only the latest and greatest content */
|
||||
| 'CONTENT_FRESH';
|
||||
|
||||
// @public
|
||||
export const DefaultTechDocsHome: (
|
||||
props: DefaultTechDocsHomeProps,
|
||||
@@ -89,7 +111,7 @@ export type DocsTableRow = {
|
||||
};
|
||||
|
||||
// @public
|
||||
export const EmbeddedDocsRouter: () => JSX.Element;
|
||||
export const EmbeddedDocsRouter: (props: PropsWithChildren<{}>) => JSX.Element;
|
||||
|
||||
// @public
|
||||
export const EntityListDocsGrid: () => JSX.Element;
|
||||
@@ -129,7 +151,9 @@ export type EntityListDocsTableProps = {
|
||||
};
|
||||
|
||||
// @public
|
||||
export const EntityTechdocsContent: () => JSX.Element;
|
||||
export const EntityTechdocsContent: (props: {
|
||||
children?: ReactNode;
|
||||
}) => JSX.Element;
|
||||
|
||||
// @public
|
||||
export const isTechDocsAvailable: (entity: Entity) => boolean;
|
||||
@@ -151,14 +175,18 @@ export interface PanelConfig {
|
||||
// @public
|
||||
export type PanelType = 'DocsCardGrid' | 'DocsTable';
|
||||
|
||||
// @public
|
||||
export const Reader: (props: ReaderProps) => JSX.Element;
|
||||
// @public @deprecated
|
||||
export const Reader: (props: TechDocsReaderPageContentProps) => JSX.Element;
|
||||
|
||||
// @public
|
||||
export type ReaderProps = {
|
||||
entityRef: CompoundEntityRef;
|
||||
withSearch?: boolean;
|
||||
onReady?: () => void;
|
||||
export type ReaderState = {
|
||||
state: ContentStateTypes;
|
||||
path: string;
|
||||
contentReload: () => void;
|
||||
content?: string;
|
||||
contentErrorMessage?: string;
|
||||
syncErrorMessage?: string;
|
||||
buildLog: string[];
|
||||
};
|
||||
|
||||
// @public
|
||||
@@ -178,19 +206,19 @@ export interface TabConfig {
|
||||
// @public
|
||||
export type TabsConfig = TabConfig[];
|
||||
|
||||
// @public
|
||||
// @public @deprecated
|
||||
export interface TechDocsApi {
|
||||
// (undocumented)
|
||||
getApiOrigin(): Promise<string>;
|
||||
// (undocumented)
|
||||
getEntityMetadata(
|
||||
entityId: CompoundEntityRef,
|
||||
): Promise<TechDocsEntityMetadata>;
|
||||
): Promise<TechDocsEntityMetadata_2>;
|
||||
// (undocumented)
|
||||
getTechDocsMetadata(entityId: CompoundEntityRef): Promise<TechDocsMetadata>;
|
||||
getTechDocsMetadata(entityId: CompoundEntityRef): Promise<TechDocsMetadata_2>;
|
||||
}
|
||||
|
||||
// @public
|
||||
// @public @deprecated
|
||||
export const techdocsApiRef: ApiRef<TechDocsApi>;
|
||||
|
||||
// @public
|
||||
@@ -208,8 +236,8 @@ export class TechDocsClient implements TechDocsApi {
|
||||
getApiOrigin(): Promise<string>;
|
||||
getEntityMetadata(
|
||||
entityId: CompoundEntityRef,
|
||||
): Promise<TechDocsEntityMetadata>;
|
||||
getTechDocsMetadata(entityId: CompoundEntityRef): Promise<TechDocsMetadata>;
|
||||
): Promise<TechDocsEntityMetadata_2>;
|
||||
getTechDocsMetadata(entityId: CompoundEntityRef): Promise<TechDocsMetadata_2>;
|
||||
}
|
||||
|
||||
// @public
|
||||
@@ -222,22 +250,14 @@ export type TechDocsCustomHomeProps = {
|
||||
tabsConfig: TabsConfig;
|
||||
};
|
||||
|
||||
// @public
|
||||
export type TechDocsEntityMetadata = Entity & {
|
||||
locationMetadata?: {
|
||||
type: string;
|
||||
target: string;
|
||||
};
|
||||
};
|
||||
// @public @deprecated (undocumented)
|
||||
export type TechDocsEntityMetadata = TechDocsEntityMetadata_2;
|
||||
|
||||
// @public
|
||||
export const TechDocsIndexPage: () => JSX.Element;
|
||||
|
||||
// @public
|
||||
export type TechDocsMetadata = {
|
||||
site_name: string;
|
||||
site_description: string;
|
||||
};
|
||||
// @public @deprecated (undocumented)
|
||||
export type TechDocsMetadata = TechDocsMetadata_2;
|
||||
|
||||
// @public
|
||||
export const TechdocsPage: () => JSX.Element;
|
||||
@@ -271,26 +291,51 @@ const techdocsPlugin: BackstagePlugin<
|
||||
export { techdocsPlugin as plugin };
|
||||
export { techdocsPlugin };
|
||||
|
||||
// @public
|
||||
export const TechDocsReaderLayout: ({
|
||||
withSearch,
|
||||
withHeader,
|
||||
}: TechDocsReaderLayoutProps) => JSX.Element;
|
||||
|
||||
// @public
|
||||
export type TechDocsReaderLayoutProps = {
|
||||
withHeader?: boolean;
|
||||
withSearch?: boolean;
|
||||
};
|
||||
|
||||
// @public
|
||||
export const TechDocsReaderPage: (
|
||||
props: TechDocsReaderPageProps,
|
||||
) => JSX.Element;
|
||||
|
||||
// @public
|
||||
export const TechDocsReaderPageContent: (
|
||||
props: TechDocsReaderPageContentProps,
|
||||
) => JSX.Element;
|
||||
|
||||
// @public
|
||||
export type TechDocsReaderPageContentProps = {
|
||||
entityRef?: CompoundEntityRef;
|
||||
withSearch?: boolean;
|
||||
onReady?: () => void;
|
||||
};
|
||||
|
||||
// @public
|
||||
export const TechDocsReaderPageHeader: (
|
||||
props: TechDocsReaderPageHeaderProps,
|
||||
) => JSX.Element;
|
||||
|
||||
// @public
|
||||
// @public @deprecated
|
||||
export type TechDocsReaderPageHeaderProps = PropsWithChildren<{
|
||||
entityRef: CompoundEntityRef;
|
||||
entityMetadata?: TechDocsEntityMetadata;
|
||||
techDocsMetadata?: TechDocsMetadata;
|
||||
entityRef?: CompoundEntityRef;
|
||||
entityMetadata?: TechDocsEntityMetadata_2;
|
||||
techDocsMetadata?: TechDocsMetadata_2;
|
||||
}>;
|
||||
|
||||
// @public
|
||||
// @public (undocumented)
|
||||
export type TechDocsReaderPageProps = {
|
||||
children?: TechDocsReaderPageRenderFunction | React_2.ReactNode;
|
||||
entityRef?: CompoundEntityRef;
|
||||
children?: TechDocsReaderPageRenderFunction | ReactNode;
|
||||
};
|
||||
|
||||
// @public
|
||||
@@ -299,12 +344,38 @@ export type TechDocsReaderPageRenderFunction = ({
|
||||
entityMetadataValue,
|
||||
entityRef,
|
||||
}: {
|
||||
techdocsMetadataValue?: TechDocsMetadata | undefined;
|
||||
entityMetadataValue?: TechDocsEntityMetadata | undefined;
|
||||
techdocsMetadataValue?: TechDocsMetadata_2 | undefined;
|
||||
entityMetadataValue?: TechDocsEntityMetadata_2 | undefined;
|
||||
entityRef: CompoundEntityRef;
|
||||
onReady: () => void;
|
||||
onReady?: () => void;
|
||||
}) => JSX.Element;
|
||||
|
||||
// @public
|
||||
export const TechDocsReaderPageSubheader: React_2.ComponentType<
|
||||
Pick<
|
||||
{
|
||||
toolbarProps?: ToolbarProps<'div', {}> | undefined;
|
||||
},
|
||||
'toolbarProps'
|
||||
> &
|
||||
StyledComponentProps<'root'>
|
||||
>;
|
||||
|
||||
// @public
|
||||
export const TechDocsReaderProvider: ({
|
||||
children,
|
||||
}: TechDocsReaderProviderProps) => JSX.Element;
|
||||
|
||||
// @public
|
||||
export type TechDocsReaderProviderProps = {
|
||||
children: TechDocsReaderProviderRenderFunction | ReactNode;
|
||||
};
|
||||
|
||||
// @public
|
||||
export type TechDocsReaderProviderRenderFunction = (
|
||||
value: ReaderState,
|
||||
) => JSX.Element;
|
||||
|
||||
// @public
|
||||
export const TechDocsSearch: (props: TechDocsSearchProps) => JSX.Element;
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ import { NotFoundError } from '@backstage/errors';
|
||||
import React from 'react';
|
||||
import { CompoundEntityRef } from '@backstage/catalog-model';
|
||||
import {
|
||||
Reader,
|
||||
TechDocsReaderPageContent,
|
||||
SyncResult,
|
||||
TechDocsStorageApi,
|
||||
techdocsStorageApiRef,
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
discoveryApiRef,
|
||||
identityApiRef,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { TechDocsReaderPageProvider } from '@backstage/plugin-techdocs-react';
|
||||
import { Header, Page, TabbedLayout } from '@backstage/core-components';
|
||||
|
||||
// used so each route can provide it's own implementation in the constructor of the react component
|
||||
@@ -112,13 +113,15 @@ function createPage({
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Reader
|
||||
<TechDocsReaderPageProvider
|
||||
entityRef={{
|
||||
kind: 'Component',
|
||||
namespace: 'default',
|
||||
name: 'my-docs',
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<TechDocsReaderPageContent />
|
||||
</TechDocsReaderPageProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,21 +38,25 @@
|
||||
"@backstage/catalog-model": "^1.0.1-next.1",
|
||||
"@backstage/config": "^1.0.0",
|
||||
"@backstage/core-components": "^0.9.3-next.1",
|
||||
"@backstage/core-app-api": "^1.0.1-next.0",
|
||||
"@backstage/core-plugin-api": "^1.0.0",
|
||||
"@backstage/errors": "^1.0.0",
|
||||
"@backstage/integration": "^1.1.0-next.1",
|
||||
"@backstage/integration-react": "^1.0.1-next.1",
|
||||
"@backstage/plugin-catalog-react": "^1.0.1-next.2",
|
||||
"@backstage/plugin-search-react": "^0.0.0",
|
||||
"@backstage/plugin-techdocs-react": "^0.0.0",
|
||||
"@backstage/theme": "^0.2.15",
|
||||
"@material-ui/core": "^4.12.2",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.57",
|
||||
"@material-ui/styles": "^4.10.0",
|
||||
"dompurify": "^2.2.9",
|
||||
"event-source-polyfill": "1.0.25",
|
||||
"event-source-polyfill": "^1.0.25",
|
||||
"git-url-parse": "^11.6.0",
|
||||
"jss": "~10.8.2",
|
||||
"lodash": "^4.17.21",
|
||||
"react-helmet": "6.1.0",
|
||||
"react-router": "6.0.0-beta.0",
|
||||
"react-router-dom": "6.0.0-beta.0",
|
||||
"react-text-truncate": "^0.18.0",
|
||||
@@ -72,6 +76,7 @@
|
||||
"@testing-library/react": "^12.1.3",
|
||||
"@testing-library/react-hooks": "^7.0.2",
|
||||
"@testing-library/user-event": "^14.0.0",
|
||||
"@types/event-source-polyfill": "^1.0.0",
|
||||
"@types/dompurify": "^2.2.2",
|
||||
"@types/jest": "^26.0.7",
|
||||
"@types/node": "^16.11.26",
|
||||
|
||||
@@ -15,21 +15,22 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { Reader } from './reader';
|
||||
import { toLowerMaybe } from './helpers';
|
||||
import { configApiRef, useApi } from '@backstage/core-plugin-api';
|
||||
|
||||
export const EntityPageDocs = ({ entity }: { entity: Entity }) => {
|
||||
const config = useApi(configApiRef);
|
||||
import { Entity, getCompoundEntityRef } from '@backstage/catalog-model';
|
||||
|
||||
import { TechDocsReaderPage } from './plugin';
|
||||
import { TechDocsReaderPageSubheader } from './reader/components/TechDocsReaderPageSubheader';
|
||||
import { TechDocsReaderPageContent } from './reader/components/TechDocsReaderPageContent';
|
||||
|
||||
type EntityPageDocsProps = { entity: Entity };
|
||||
|
||||
export const EntityPageDocs = ({ entity }: EntityPageDocsProps) => {
|
||||
const entityRef = getCompoundEntityRef(entity);
|
||||
|
||||
return (
|
||||
<Reader
|
||||
withSearch={false}
|
||||
entityRef={{
|
||||
namespace: toLowerMaybe(entity.metadata.namespace ?? 'default', config),
|
||||
kind: toLowerMaybe(entity.kind, config),
|
||||
name: toLowerMaybe(entity.metadata.name, config),
|
||||
}}
|
||||
/>
|
||||
<TechDocsReaderPage entityRef={entityRef}>
|
||||
<TechDocsReaderPageSubheader />
|
||||
<TechDocsReaderPageContent withSearch={false} />
|
||||
</TechDocsReaderPage>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -14,14 +14,17 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { useEntity } from '@backstage/plugin-catalog-react';
|
||||
import React, { PropsWithChildren } from 'react';
|
||||
import { Route, Routes } from 'react-router-dom';
|
||||
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { FlatRoutes } from '@backstage/core-app-api';
|
||||
import { useEntity } from '@backstage/plugin-catalog-react';
|
||||
import { MissingAnnotationEmptyState } from '@backstage/core-components';
|
||||
|
||||
import { EntityPageDocs } from './EntityPageDocs';
|
||||
import { TechDocsIndexPage } from './home/components/TechDocsIndexPage';
|
||||
import { TechDocsReaderPage } from './reader/components/TechDocsReaderPage';
|
||||
import { EntityPageDocs } from './EntityPageDocs';
|
||||
import { MissingAnnotationEmptyState } from '@backstage/core-components';
|
||||
|
||||
const TECHDOCS_ANNOTATION = 'backstage.io/techdocs-ref';
|
||||
|
||||
@@ -55,7 +58,8 @@ export const Router = () => {
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const EmbeddedDocsRouter = () => {
|
||||
export const EmbeddedDocsRouter = (props: PropsWithChildren<{}>) => {
|
||||
const { children } = props;
|
||||
const { entity } = useEntity();
|
||||
|
||||
const projectId = entity.metadata.annotations?.[TECHDOCS_ANNOTATION];
|
||||
@@ -65,8 +69,10 @@ export const EmbeddedDocsRouter = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="/*" element={<EntityPageDocs entity={entity} />} />
|
||||
</Routes>
|
||||
<FlatRoutes>
|
||||
<Route path="/*" element={<EntityPageDocs entity={entity} />}>
|
||||
{children}
|
||||
</Route>
|
||||
</FlatRoutes>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -15,7 +15,10 @@
|
||||
*/
|
||||
|
||||
import { CompoundEntityRef } from '@backstage/catalog-model';
|
||||
import { TechDocsEntityMetadata, TechDocsMetadata } from './types';
|
||||
import {
|
||||
TechDocsEntityMetadata,
|
||||
TechDocsMetadata,
|
||||
} from '@backstage/plugin-techdocs-react';
|
||||
import { createApiRef } from '@backstage/core-plugin-api';
|
||||
|
||||
/**
|
||||
@@ -31,6 +34,7 @@ export const techdocsStorageApiRef = createApiRef<TechDocsStorageApi>({
|
||||
* Utility API reference for the {@link TechDocsApi}.
|
||||
*
|
||||
* @public
|
||||
* @deprecated Import from `@backstage/plugin-techdocs-react` instead
|
||||
*/
|
||||
export const techdocsApiRef = createApiRef<TechDocsApi>({
|
||||
id: 'plugin.techdocs.service',
|
||||
@@ -68,6 +72,7 @@ export interface TechDocsStorageApi {
|
||||
* API to talk to techdocs-backend.
|
||||
*
|
||||
* @public
|
||||
* @deprecated Import from `@backstage/plugin-techdocs-react` instead
|
||||
*/
|
||||
export interface TechDocsApi {
|
||||
getApiOrigin(): Promise<string>;
|
||||
|
||||
@@ -22,9 +22,12 @@ import {
|
||||
IdentityApi,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { NotFoundError, ResponseError } from '@backstage/errors';
|
||||
import {
|
||||
TechDocsEntityMetadata,
|
||||
TechDocsMetadata,
|
||||
} from '@backstage/plugin-techdocs-react';
|
||||
import { EventSourcePolyfill } from 'event-source-polyfill';
|
||||
import { SyncResult, TechDocsApi, TechDocsStorageApi } from './api';
|
||||
import { TechDocsEntityMetadata, TechDocsMetadata } from './types';
|
||||
|
||||
/**
|
||||
* API to talk to `techdocs-backend`.
|
||||
|
||||
@@ -20,6 +20,11 @@
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
import {
|
||||
TechDocsEntityMetadata,
|
||||
TechDocsMetadata,
|
||||
} from '@backstage/plugin-techdocs-react';
|
||||
|
||||
export * from './types';
|
||||
export * from './api';
|
||||
export * from './client';
|
||||
@@ -36,3 +41,22 @@ export {
|
||||
techdocsPlugin,
|
||||
} from './plugin';
|
||||
export * from './Router';
|
||||
|
||||
/**
|
||||
* @deprecated Import from `@backstage/plugin-techdocs-react` instead
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
type DeprecatedTechDocsMetadata = TechDocsMetadata;
|
||||
|
||||
/**
|
||||
* @deprecated Import from `@backstage/plugin-techdocs-react` instead
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
type DeprecatedTechDocsEntityMetadata = TechDocsEntityMetadata;
|
||||
|
||||
export type {
|
||||
DeprecatedTechDocsEntityMetadata as TechDocsEntityMetadata,
|
||||
DeprecatedTechDocsMetadata as TechDocsMetadata,
|
||||
};
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 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 { ConfigReader } from '@backstage/config';
|
||||
import {
|
||||
ScmIntegrationsApi,
|
||||
scmIntegrationsApiRef,
|
||||
} from '@backstage/integration-react';
|
||||
import { TestApiRegistry, wrapInTestApp } from '@backstage/test-utils';
|
||||
import { act, render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { TechDocsStorageApi, techdocsStorageApiRef } from '../../api';
|
||||
import { Reader } from './Reader';
|
||||
import { ApiProvider } from '@backstage/core-app-api';
|
||||
import { searchApiRef } from '@backstage/plugin-search-react';
|
||||
|
||||
jest.mock('react-router-dom', () => {
|
||||
const actual = jest.requireActual('react-router-dom');
|
||||
return {
|
||||
...actual,
|
||||
useParams: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
const { useParams }: { useParams: jest.Mock } =
|
||||
jest.requireMock('react-router-dom');
|
||||
|
||||
describe('<Reader />', () => {
|
||||
it('should render Reader content', async () => {
|
||||
useParams.mockReturnValue({
|
||||
entityRef: 'Component::backstage',
|
||||
});
|
||||
|
||||
const scmIntegrationsApi: ScmIntegrationsApi =
|
||||
ScmIntegrationsApi.fromConfig(
|
||||
new ConfigReader({
|
||||
integrations: {},
|
||||
}),
|
||||
);
|
||||
const techdocsStorageApi: Partial<TechDocsStorageApi> = {};
|
||||
const searchApi = {
|
||||
query: () =>
|
||||
Promise.resolve({
|
||||
results: [],
|
||||
}),
|
||||
};
|
||||
const apiRegistry = TestApiRegistry.from(
|
||||
[scmIntegrationsApiRef, scmIntegrationsApi],
|
||||
[techdocsStorageApiRef, techdocsStorageApi],
|
||||
[searchApiRef, searchApi],
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
const rendered = render(
|
||||
wrapInTestApp(
|
||||
<ApiProvider apis={apiRegistry}>
|
||||
<Reader
|
||||
entityRef={{
|
||||
kind: 'Component',
|
||||
namespace: 'default',
|
||||
name: 'example',
|
||||
}}
|
||||
/>
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
expect(
|
||||
rendered.getByTestId('techdocs-content-shadowroot'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,953 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 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, {
|
||||
PropsWithChildren,
|
||||
ComponentType,
|
||||
createContext,
|
||||
useContext,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import {
|
||||
Grid,
|
||||
makeStyles,
|
||||
useTheme,
|
||||
Theme,
|
||||
lighten,
|
||||
alpha,
|
||||
} from '@material-ui/core';
|
||||
|
||||
import { CompoundEntityRef } from '@backstage/catalog-model';
|
||||
import { useApi, configApiRef } from '@backstage/core-plugin-api';
|
||||
import { scmIntegrationsApiRef } from '@backstage/integration-react';
|
||||
import { BackstageTheme } from '@backstage/theme';
|
||||
import {
|
||||
sidebarConfig,
|
||||
SidebarPinStateContext,
|
||||
} from '@backstage/core-components';
|
||||
|
||||
import { techdocsStorageApiRef } from '../../api';
|
||||
|
||||
import {
|
||||
addBaseUrl,
|
||||
addGitFeedbackLink,
|
||||
addLinkClickListener,
|
||||
addSidebarToggle,
|
||||
injectCss,
|
||||
onCssReady,
|
||||
removeMkdocsHeader,
|
||||
rewriteDocLinks,
|
||||
sanitizeDOM,
|
||||
simplifyMkdocsFooter,
|
||||
scrollIntoAnchor,
|
||||
transform as transformer,
|
||||
copyToClipboard,
|
||||
} from '../transformers';
|
||||
|
||||
import { TechDocsSearch } from '../../search';
|
||||
import { TechDocsStateIndicator } from './TechDocsStateIndicator';
|
||||
import { useReaderState } from './useReaderState';
|
||||
|
||||
/**
|
||||
* Props for {@link Reader}
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type ReaderProps = {
|
||||
entityRef: CompoundEntityRef;
|
||||
withSearch?: boolean;
|
||||
onReady?: () => void;
|
||||
};
|
||||
|
||||
const useStyles = makeStyles<BackstageTheme>(theme => ({
|
||||
searchBar: {
|
||||
maxWidth: 'calc(100% - 16rem * 2 - 2.4rem)',
|
||||
marginTop: 0,
|
||||
marginBottom: theme.spacing(1),
|
||||
marginLeft: 'calc(16rem + 1.2rem)',
|
||||
'@media screen and (max-width: 76.1875em)': {
|
||||
marginLeft: '0',
|
||||
maxWidth: '100%',
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
type TechDocsReaderValue = ReturnType<typeof useReaderState>;
|
||||
|
||||
const TechDocsReaderContext = createContext<TechDocsReaderValue>(
|
||||
{} as TechDocsReaderValue,
|
||||
);
|
||||
|
||||
const TechDocsReaderProvider = ({
|
||||
children,
|
||||
entityRef,
|
||||
}: PropsWithChildren<{ entityRef: CompoundEntityRef }>) => {
|
||||
const { '*': path } = useParams();
|
||||
const { kind, namespace, name } = entityRef;
|
||||
const value = useReaderState(kind, namespace, name, path);
|
||||
return (
|
||||
<TechDocsReaderContext.Provider value={value}>
|
||||
{children}
|
||||
</TechDocsReaderContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Note: this HOC is currently being exported so that we can rapidly
|
||||
* iterate on alternative <Reader /> implementations that extend core
|
||||
* functionality. There is no guarantee that this HOC will continue to be
|
||||
* exported by the package in the future!
|
||||
*
|
||||
* todo: Make public or stop exporting (ctrl+f "altReaderExperiments")
|
||||
* @internal
|
||||
*/
|
||||
export const withTechDocsReaderProvider =
|
||||
<T extends {}>(Component: ComponentType<T>, entityRef: CompoundEntityRef) =>
|
||||
(props: T) =>
|
||||
(
|
||||
<TechDocsReaderProvider entityRef={entityRef}>
|
||||
<Component {...props} />
|
||||
</TechDocsReaderProvider>
|
||||
);
|
||||
|
||||
/**
|
||||
* Note: this hook is currently being exported so that we can rapidly
|
||||
* iterate on alternative <Reader /> implementations that extend core
|
||||
* functionality. There is no guarantee that this hook will continue to be
|
||||
* exported by the package in the future!
|
||||
*
|
||||
* todo: Make public or stop exporting (ctrl+f "altReaderExperiments")
|
||||
* @internal
|
||||
*/
|
||||
export const useTechDocsReader = () => useContext(TechDocsReaderContext);
|
||||
|
||||
type TypographyHeadings = Pick<
|
||||
Theme['typography'],
|
||||
'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'
|
||||
>;
|
||||
|
||||
type TypographyHeadingsKeys = keyof TypographyHeadings;
|
||||
|
||||
const headings: TypographyHeadingsKeys[] = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
|
||||
|
||||
/**
|
||||
* Hook that encapsulates the behavior of getting raw HTML and applying
|
||||
* transforms to it in order to make it function at a basic level in the
|
||||
* Backstage UI.
|
||||
*
|
||||
* Note: this hook is currently being exported so that we can rapidly iterate
|
||||
* on alternative <Reader /> implementations that extend core functionality.
|
||||
* There is no guarantee that this hook will continue to be exported by the
|
||||
* package in the future!
|
||||
*
|
||||
* todo: Make public or stop exporting (see others: "altReaderExperiments")
|
||||
* @internal
|
||||
*/
|
||||
export const useTechDocsReaderDom = (
|
||||
entityRef: CompoundEntityRef,
|
||||
): Element | null => {
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme<BackstageTheme>();
|
||||
const techdocsStorageApi = useApi(techdocsStorageApiRef);
|
||||
const scmIntegrationsApi = useApi(scmIntegrationsApiRef);
|
||||
const techdocsSanitizer = useApi(configApiRef);
|
||||
const { namespace = '', kind = '', name = '' } = entityRef;
|
||||
const { state, path, content: rawPage } = useTechDocsReader();
|
||||
const isDarkTheme = theme.palette.type === 'dark';
|
||||
|
||||
const [sidebars, setSidebars] = useState<HTMLElement[]>();
|
||||
const [dom, setDom] = useState<HTMLElement | null>(null);
|
||||
|
||||
// sidebar pinned status to be used in computing CSS style injections
|
||||
const { isPinned } = useContext(SidebarPinStateContext);
|
||||
|
||||
const updateSidebarPosition = useCallback(() => {
|
||||
if (!dom || !sidebars) return;
|
||||
// set sidebar height so they don't initially render in wrong position
|
||||
const mdTabs = dom.querySelector('.md-container > .md-tabs');
|
||||
const sidebarsCollapsed = window.matchMedia(
|
||||
'screen and (max-width: 76.1875em)',
|
||||
).matches;
|
||||
const newTop = Math.max(dom.getBoundingClientRect().top, 0);
|
||||
sidebars.forEach(sidebar => {
|
||||
if (sidebarsCollapsed) {
|
||||
sidebar.style.top = '0px';
|
||||
} else if (mdTabs) {
|
||||
sidebar.style.top = `${
|
||||
newTop + mdTabs.getBoundingClientRect().height
|
||||
}px`;
|
||||
} else {
|
||||
sidebar.style.top = `${newTop}px`;
|
||||
}
|
||||
});
|
||||
}, [dom, sidebars]);
|
||||
|
||||
useEffect(() => {
|
||||
updateSidebarPosition();
|
||||
window.addEventListener('scroll', updateSidebarPosition, true);
|
||||
window.addEventListener('resize', updateSidebarPosition);
|
||||
return () => {
|
||||
window.removeEventListener('scroll', updateSidebarPosition, true);
|
||||
window.removeEventListener('resize', updateSidebarPosition);
|
||||
};
|
||||
// an update to "state" might lead to an updated UI so we include it as a trigger
|
||||
}, [updateSidebarPosition, state]);
|
||||
|
||||
// dynamically set width of footer to accommodate for pinning of the sidebar
|
||||
const updateFooterWidth = useCallback(() => {
|
||||
if (!dom) return;
|
||||
const footer = dom.querySelector('.md-footer') as HTMLElement;
|
||||
if (footer) {
|
||||
footer.style.width = `${dom.getBoundingClientRect().width}px`;
|
||||
}
|
||||
}, [dom]);
|
||||
|
||||
useEffect(() => {
|
||||
updateFooterWidth();
|
||||
window.addEventListener('resize', updateFooterWidth);
|
||||
return () => {
|
||||
window.removeEventListener('resize', updateFooterWidth);
|
||||
};
|
||||
});
|
||||
|
||||
// a function that performs transformations that are executed prior to adding it to the DOM
|
||||
const preRender = useCallback(
|
||||
(rawContent: string, contentPath: string) =>
|
||||
transformer(rawContent, [
|
||||
sanitizeDOM(techdocsSanitizer.getOptionalConfig('techdocs.sanitizer')),
|
||||
addBaseUrl({
|
||||
techdocsStorageApi,
|
||||
entityId: {
|
||||
kind,
|
||||
name,
|
||||
namespace,
|
||||
},
|
||||
path: contentPath,
|
||||
}),
|
||||
rewriteDocLinks(),
|
||||
addSidebarToggle(),
|
||||
removeMkdocsHeader(),
|
||||
simplifyMkdocsFooter(),
|
||||
addGitFeedbackLink(scmIntegrationsApi),
|
||||
injectCss({
|
||||
// Variables
|
||||
css: `
|
||||
/*
|
||||
As the MkDocs output is rendered in shadow DOM, the CSS variable definitions on the root selector are not applied. Instead, they have to be applied on :host.
|
||||
As there is no way to transform the served main*.css yet (for example in the backend), we have to copy from main*.css and modify them.
|
||||
*/
|
||||
:host {
|
||||
/* FONT */
|
||||
--md-default-fg-color: ${theme.palette.text.primary};
|
||||
--md-default-fg-color--light: ${theme.palette.text.secondary};
|
||||
--md-default-fg-color--lighter: ${lighten(
|
||||
theme.palette.text.secondary,
|
||||
0.7,
|
||||
)};
|
||||
--md-default-fg-color--lightest: ${lighten(
|
||||
theme.palette.text.secondary,
|
||||
0.3,
|
||||
)};
|
||||
|
||||
/* BACKGROUND */
|
||||
--md-default-bg-color:${theme.palette.background.default};
|
||||
--md-default-bg-color--light: ${theme.palette.background.paper};
|
||||
--md-default-bg-color--lighter: ${lighten(
|
||||
theme.palette.background.paper,
|
||||
0.7,
|
||||
)};
|
||||
--md-default-bg-color--lightest: ${lighten(
|
||||
theme.palette.background.paper,
|
||||
0.3,
|
||||
)};
|
||||
|
||||
/* PRIMARY */
|
||||
--md-primary-fg-color: ${theme.palette.primary.main};
|
||||
--md-primary-fg-color--light: ${theme.palette.primary.light};
|
||||
--md-primary-fg-color--dark: ${theme.palette.primary.dark};
|
||||
--md-primary-bg-color: ${theme.palette.primary.contrastText};
|
||||
--md-primary-bg-color--light: ${lighten(
|
||||
theme.palette.primary.contrastText,
|
||||
0.7,
|
||||
)};
|
||||
|
||||
/* ACCENT */
|
||||
--md-accent-fg-color: var(--md-primary-fg-color);
|
||||
|
||||
/* SHADOW */
|
||||
--md-shadow-z1: ${theme.shadows[1]};
|
||||
--md-shadow-z2: ${theme.shadows[2]};
|
||||
--md-shadow-z3: ${theme.shadows[3]};
|
||||
|
||||
/* EXTENSIONS */
|
||||
--md-admonition-fg-color: var(--md-default-fg-color);
|
||||
--md-admonition-bg-color: var(--md-default-bg-color);
|
||||
/* Admonitions and others are using SVG masks to define icons. These masks are defined as CSS variables. */
|
||||
--md-admonition-icon--note: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20.71 7.04c.39-.39.39-1.04 0-1.41l-2.34-2.34c-.37-.39-1.02-.39-1.41 0l-1.84 1.83 3.75 3.75M3 17.25V21h3.75L17.81 9.93l-3.75-3.75L3 17.25z"/></svg>');
|
||||
--md-admonition-icon--abstract: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M4 5h16v2H4V5m0 4h16v2H4V9m0 4h16v2H4v-2m0 4h10v2H4v-2z"/></svg>');
|
||||
--md-admonition-icon--info: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13 9h-2V7h2m0 10h-2v-6h2m-1-9A10 10 0 002 12a10 10 0 0010 10 10 10 0 0010-10A10 10 0 0012 2z"/></svg>');
|
||||
--md-admonition-icon--tip: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M17.55 11.2c-.23-.3-.5-.56-.76-.82-.65-.6-1.4-1.03-2.03-1.66C13.3 7.26 13 4.85 13.91 3c-.91.23-1.75.75-2.45 1.32-2.54 2.08-3.54 5.75-2.34 8.9.04.1.08.2.08.33 0 .22-.15.42-.35.5-.22.1-.46.04-.64-.12a.83.83 0 01-.15-.17c-1.1-1.43-1.28-3.48-.53-5.12C5.89 10 5 12.3 5.14 14.47c.04.5.1 1 .27 1.5.14.6.4 1.2.72 1.73 1.04 1.73 2.87 2.97 4.84 3.22 2.1.27 4.35-.12 5.96-1.6 1.8-1.66 2.45-4.32 1.5-6.6l-.13-.26c-.2-.46-.47-.87-.8-1.25l.05-.01m-3.1 6.3c-.28.24-.73.5-1.08.6-1.1.4-2.2-.16-2.87-.82 1.19-.28 1.89-1.16 2.09-2.05.17-.8-.14-1.46-.27-2.23-.12-.74-.1-1.37.18-2.06.17.38.37.76.6 1.06.76 1 1.95 1.44 2.2 2.8.04.14.06.28.06.43.03.82-.32 1.72-.92 2.27h.01z"/></svg>');
|
||||
--md-admonition-icon--success: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2m-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>');
|
||||
--md-admonition-icon--question: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M15.07 11.25l-.9.92C13.45 12.89 13 13.5 13 15h-2v-.5c0-1.11.45-2.11 1.17-2.83l1.24-1.26c.37-.36.59-.86.59-1.41a2 2 0 00-2-2 2 2 0 00-2 2H8a4 4 0 014-4 4 4 0 014 4 3.2 3.2 0 01-.93 2.25M13 19h-2v-2h2M12 2A10 10 0 002 12a10 10 0 0010 10 10 10 0 0010-10c0-5.53-4.5-10-10-10z"/></svg>');
|
||||
--md-admonition-icon--warning: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13 14h-2v-4h2m0 8h-2v-2h2M1 21h22L12 2 1 21z"/></svg>');
|
||||
--md-admonition-icon--failure: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 2c5.53 0 10 4.47 10 10s-4.47 10-10 10S2 17.53 2 12 6.47 2 12 2m3.59 5L12 10.59 8.41 7 7 8.41 10.59 12 7 15.59 8.41 17 12 13.41 15.59 17 17 15.59 13.41 12 17 8.41 15.59 7z"/></svg>');
|
||||
--md-admonition-icon--danger: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M11.5 20l4.86-9.73H13V4l-5 9.73h3.5V20M12 2c2.75 0 5.1 1 7.05 2.95C21 6.9 22 9.25 22 12s-1 5.1-2.95 7.05C17.1 21 14.75 22 12 22s-5.1-1-7.05-2.95C3 17.1 2 14.75 2 12s1-5.1 2.95-7.05C6.9 3 9.25 2 12 2z"/></svg>');
|
||||
--md-admonition-icon--bug: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M14 12h-4v-2h4m0 6h-4v-2h4m6-6h-2.81a5.985 5.985 0 00-1.82-1.96L17 4.41 15.59 3l-2.17 2.17a6.002 6.002 0 00-2.83 0L8.41 3 7 4.41l1.62 1.63C7.88 6.55 7.26 7.22 6.81 8H4v2h2.09c-.05.33-.09.66-.09 1v1H4v2h2v1c0 .34.04.67.09 1H4v2h2.81c1.04 1.79 2.97 3 5.19 3s4.15-1.21 5.19-3H20v-2h-2.09c.05-.33.09-.66.09-1v-1h2v-2h-2v-1c0-.34-.04-.67-.09-1H20V8z"/></svg>');
|
||||
--md-admonition-icon--example: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M7 13v-2h14v2H7m0 6v-2h14v2H7M7 7V5h14v2H7M3 8V5H2V4h2v4H3m-1 9v-1h3v4H2v-1h2v-.5H3v-1h1V17H2m2.25-7a.75.75 0 01.75.75c0 .2-.08.39-.21.52L3.12 13H5v1H2v-.92L4 11H2v-1h2.25z"/></svg>');
|
||||
--md-admonition-icon--quote: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M14 17h3l2-4V7h-6v6h3M6 17h3l2-4V7H5v6h3l-2 4z"/></svg>');
|
||||
--md-footnotes-icon: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19 7v4H5.83l3.58-3.59L8 6l-6 6 6 6 1.41-1.42L5.83 13H21V7h-2z"/></svg>');
|
||||
--md-details-icon: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M8.59 16.58 13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.42z"/></svg>');
|
||||
--md-tasklist-icon: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 5v14H5V5h14m0-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"/></svg>');
|
||||
--md-tasklist-icon--checked: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.11 0 2-.9 2-2V5c0-1.1-.89-2-2-2zm-9 14l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>');
|
||||
--md-nav-icon--prev: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 11v2H8l5.5 5.5-1.42 1.42L4.16 12l7.92-7.92L13.5 5.5 8 11h12z"/></svg>');
|
||||
--md-nav-icon--next: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M8.59 16.58 13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.42z"/></svg>');
|
||||
--md-toc-icon: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3 9h14V7H3v2m0 4h14v-2H3v2m0 4h14v-2H3v2m16 0h2v-2h-2v2m0-10v2h2V7h-2m0 6h2v-2h-2v2z"/></svg>');
|
||||
--md-clipboard-icon: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19 21H8V7h11m0-2H8a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h11a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2m-3-4H4a2 2 0 0 0-2 2v14h2V3h12V1z"/></svg>');
|
||||
--md-search-result-icon: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h7c-.41-.25-.8-.56-1.14-.9-.33-.33-.61-.7-.86-1.1H6V4h7v5h5v1.18c.71.16 1.39.43 2 .82V8l-6-6m6.31 16.9c1.33-2.11.69-4.9-1.4-6.22-2.11-1.33-4.91-.68-6.22 1.4-1.34 2.11-.69 4.89 1.4 6.22 1.46.93 3.32.93 4.79.02L22 23.39 23.39 22l-3.08-3.1m-3.81.1a2.5 2.5 0 0 1-2.5-2.5 2.5 2.5 0 0 1 2.5-2.5 2.5 2.5 0 0 1 2.5 2.5 2.5 2.5 0 0 1-2.5 2.5z"/></svg>');
|
||||
--md-source-forks-icon: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M5 3.25a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0zm0 2.122a2.25 2.25 0 1 0-1.5 0v.878A2.25 2.25 0 0 0 5.75 8.5h1.5v2.128a2.251 2.251 0 1 0 1.5 0V8.5h1.5a2.25 2.25 0 0 0 2.25-2.25v-.878a2.25 2.25 0 1 0-1.5 0v.878a.75.75 0 0 1-.75.75h-4.5A.75.75 0 0 1 5 6.25v-.878zm3.75 7.378a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0zm3-8.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5z"/></svg>');
|
||||
--md-source-repositories-icon: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M2 2.5A2.5 2.5 0 0 1 4.5 0h8.75a.75.75 0 0 1 .75.75v12.5a.75.75 0 0 1-.75.75h-2.5a.75.75 0 1 1 0-1.5h1.75v-2h-8a1 1 0 0 0-.714 1.7.75.75 0 0 1-1.072 1.05A2.495 2.495 0 0 1 2 11.5v-9zm10.5-1V9h-8c-.356 0-.694.074-1 .208V2.5a1 1 0 0 1 1-1h8zM5 12.25v3.25a.25.25 0 0 0 .4.2l1.45-1.087a.25.25 0 0 1 .3 0L8.6 15.7a.25.25 0 0 0 .4-.2v-3.25a.25.25 0 0 0-.25-.25h-3.5a.25.25 0 0 0-.25.25z"/></svg>');
|
||||
--md-source-stars-icon: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.75.75 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25zm0 2.445L6.615 5.5a.75.75 0 0 1-.564.41l-3.097.45 2.24 2.184a.75.75 0 0 1 .216.664l-.528 3.084 2.769-1.456a.75.75 0 0 1 .698 0l2.77 1.456-.53-3.084a.75.75 0 0 1 .216-.664l2.24-2.183-3.096-.45a.75.75 0 0 1-.564-.41L8 2.694v.001z"/></svg>');
|
||||
--md-source-version-icon: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M2.5 7.775V2.75a.25.25 0 0 1 .25-.25h5.025a.25.25 0 0 1 .177.073l6.25 6.25a.25.25 0 0 1 0 .354l-5.025 5.025a.25.25 0 0 1-.354 0l-6.25-6.25a.25.25 0 0 1-.073-.177zm-1.5 0V2.75C1 1.784 1.784 1 2.75 1h5.025c.464 0 .91.184 1.238.513l6.25 6.25a1.75 1.75 0 0 1 0 2.474l-5.026 5.026a1.75 1.75 0 0 1-2.474 0l-6.25-6.25A1.75 1.75 0 0 1 1 7.775zM6 5a1 1 0 1 0 0 2 1 1 0 0 0 0-2z"/></svg>');
|
||||
--md-version-icon: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><!--! Font Awesome Free 6.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2022 Fonticons, Inc.--><path d="m310.6 246.6-127.1 128c-7.1 6.3-15.3 9.4-23.5 9.4s-16.38-3.125-22.63-9.375l-127.1-128C.224 237.5-2.516 223.7 2.438 211.8S19.07 192 32 192h255.1c12.94 0 24.62 7.781 29.58 19.75s3.12 25.75-6.08 34.85z"/></svg>');
|
||||
}
|
||||
|
||||
:host > * {
|
||||
/* CODE */
|
||||
--md-code-fg-color: ${theme.palette.text.primary};
|
||||
--md-code-bg-color: ${theme.palette.background.paper};
|
||||
--md-code-hl-color: ${alpha(theme.palette.warning.main, 0.5)};
|
||||
--md-code-hl-keyword-color: ${
|
||||
isDarkTheme
|
||||
? theme.palette.primary.light
|
||||
: theme.palette.primary.dark
|
||||
};
|
||||
--md-code-hl-function-color: ${
|
||||
isDarkTheme
|
||||
? theme.palette.secondary.light
|
||||
: theme.palette.secondary.dark
|
||||
};
|
||||
--md-code-hl-string-color: ${
|
||||
isDarkTheme
|
||||
? theme.palette.success.light
|
||||
: theme.palette.success.dark
|
||||
};
|
||||
--md-code-hl-number-color: ${
|
||||
isDarkTheme ? theme.palette.error.light : theme.palette.error.dark
|
||||
};
|
||||
--md-code-hl-constant-color: var(--md-code-hl-function-color);
|
||||
--md-code-hl-special-color: var(--md-code-hl-function-color);
|
||||
--md-code-hl-name-color: var(--md-code-fg-color);
|
||||
--md-code-hl-comment-color: var(--md-default-fg-color--light);
|
||||
--md-code-hl-generic-color: var(--md-default-fg-color--light);
|
||||
--md-code-hl-variable-color: var(--md-default-fg-color--light);
|
||||
--md-code-hl-operator-color: var(--md-default-fg-color--light);
|
||||
--md-code-hl-punctuation-color: var(--md-default-fg-color--light);
|
||||
|
||||
/* TYPESET */
|
||||
--md-typeset-font-size: 1rem;
|
||||
--md-typeset-color: var(--md-default-fg-color);
|
||||
--md-typeset-a-color: var(--md-accent-fg-color);
|
||||
--md-typeset-table-color: ${theme.palette.text.primary};
|
||||
--md-typeset-del-color: ${
|
||||
isDarkTheme
|
||||
? alpha(theme.palette.error.dark, 0.5)
|
||||
: alpha(theme.palette.error.light, 0.5)
|
||||
};
|
||||
--md-typeset-ins-color: ${
|
||||
isDarkTheme
|
||||
? alpha(theme.palette.success.dark, 0.5)
|
||||
: alpha(theme.palette.success.light, 0.5)
|
||||
};
|
||||
--md-typeset-mark-color: ${
|
||||
isDarkTheme
|
||||
? alpha(theme.palette.warning.dark, 0.5)
|
||||
: alpha(theme.palette.warning.light, 0.5)
|
||||
};
|
||||
}
|
||||
|
||||
@media screen and (max-width: 76.1875em) {
|
||||
:host > * {
|
||||
/* TYPESET */
|
||||
--md-typeset-font-size: .9rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
:host > * {
|
||||
/* TYPESET */
|
||||
--md-typeset-font-size: .7rem;
|
||||
}
|
||||
}
|
||||
`,
|
||||
}),
|
||||
injectCss({
|
||||
// Reset
|
||||
css: `
|
||||
body {
|
||||
--md-text-color: var(--md-default-fg-color);
|
||||
--md-text-link-color: var(--md-accent-fg-color);
|
||||
--md-text-font-family: ${theme.typography.fontFamily};
|
||||
font-family: var(--md-text-font-family);
|
||||
background-color: unset;
|
||||
}
|
||||
`,
|
||||
}),
|
||||
injectCss({
|
||||
// Layout
|
||||
css: `
|
||||
.md-grid {
|
||||
max-width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.md-nav {
|
||||
font-size: calc(var(--md-typeset-font-size) * 0.9);
|
||||
}
|
||||
.md-nav__link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.md-nav__icon {
|
||||
height: 20px !important;
|
||||
width: 20px !important;
|
||||
margin-left:${theme.spacing(1)}px;
|
||||
}
|
||||
.md-nav__icon svg {
|
||||
margin: 0;
|
||||
width: 20px !important;
|
||||
height: 20px !important;
|
||||
}
|
||||
.md-nav__icon:after {
|
||||
width: 20px !important;
|
||||
height: 20px !important;
|
||||
}
|
||||
|
||||
.md-main__inner {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.md-sidebar {
|
||||
bottom: 75px;
|
||||
position: fixed;
|
||||
width: 16rem;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
scrollbar-color: rgb(193, 193, 193) #eee;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
.md-sidebar::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
}
|
||||
.md-sidebar::-webkit-scrollbar-button {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
}
|
||||
.md-sidebar::-webkit-scrollbar-track {
|
||||
background: #eee;
|
||||
border: 1 px solid rgb(250, 250, 250);
|
||||
box-shadow: 0px 0px 3px #dfdfdf inset;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.md-sidebar::-webkit-scrollbar-thumb {
|
||||
width: 5px;
|
||||
background: rgb(193, 193, 193);
|
||||
border: transparent;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.md-sidebar::-webkit-scrollbar-thumb:hover {
|
||||
background: rgb(125, 125, 125);
|
||||
}
|
||||
.md-sidebar--secondary {
|
||||
right: ${theme.spacing(3)}px;
|
||||
}
|
||||
.md-sidebar__scrollwrap {
|
||||
overflow: unset !important;
|
||||
}
|
||||
|
||||
.md-content {
|
||||
max-width: calc(100% - 16rem * 2);
|
||||
margin-left: 16rem;
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
|
||||
.md-footer {
|
||||
position: fixed;
|
||||
bottom: 0px;
|
||||
}
|
||||
.md-footer__title {
|
||||
background-color: unset;
|
||||
}
|
||||
.md-footer__link, .md-footer-nav__link {
|
||||
width: 16rem;
|
||||
}
|
||||
|
||||
.md-dialog {
|
||||
background-color: unset;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 76.25em) {
|
||||
.md-sidebar {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 76.1875em) {
|
||||
.md-nav {
|
||||
transition: none !important;
|
||||
background-color: var(--md-default-bg-color)
|
||||
}
|
||||
.md-nav--primary .md-nav__title {
|
||||
cursor: auto;
|
||||
color: var(--md-default-fg-color);
|
||||
font-weight: 700;
|
||||
white-space: normal;
|
||||
line-height: 1rem;
|
||||
height: auto;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
row-gap: 1.6rem;
|
||||
padding: 1.2rem .8rem .8rem;
|
||||
background-color: var(--md-default-bg-color);
|
||||
}
|
||||
.md-nav--primary .md-nav__title~.md-nav__list {
|
||||
box-shadow: none;
|
||||
}
|
||||
.md-nav--primary .md-nav__title ~ .md-nav__list > :first-child {
|
||||
border-top: none;
|
||||
}
|
||||
.md-nav--primary .md-nav__title .md-nav__button {
|
||||
display: none;
|
||||
}
|
||||
.md-nav--primary .md-nav__title .md-nav__icon {
|
||||
color: var(--md-default-fg-color);
|
||||
position: static;
|
||||
height: auto;
|
||||
margin: 0 0 0 -0.2rem;
|
||||
}
|
||||
.md-nav--primary > .md-nav__title [for="none"] {
|
||||
padding-top: 0;
|
||||
}
|
||||
.md-nav--primary .md-nav__item {
|
||||
border-top: none;
|
||||
}
|
||||
.md-nav--primary :is(.md-nav__title,.md-nav__item) {
|
||||
font-size : var(--md-typeset-font-size);
|
||||
}
|
||||
.md-nav .md-source {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.md-sidebar {
|
||||
height: 100%;
|
||||
}
|
||||
.md-sidebar--primary {
|
||||
width: 16rem !important;
|
||||
z-index: 200;
|
||||
left: ${
|
||||
isPinned
|
||||
? `calc(-16rem + ${sidebarConfig.drawerWidthOpen}px)`
|
||||
: `calc(-16rem + ${sidebarConfig.drawerWidthClosed}px)`
|
||||
} !important;
|
||||
}
|
||||
.md-sidebar--secondary:not([hidden]) {
|
||||
display: none;
|
||||
}
|
||||
[data-md-toggle=drawer]:checked~.md-container .md-sidebar--primary {
|
||||
transform: translateX(16rem);
|
||||
}
|
||||
|
||||
.md-content {
|
||||
max-width: 100%;
|
||||
margin-left: 0;
|
||||
}
|
||||
.md-content__inner {
|
||||
margin: 0;
|
||||
}
|
||||
.md-content__inner .highlighttable {
|
||||
max-width: 100%;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.md-header__button {
|
||||
margin: 0.4rem 0;
|
||||
margin-left: 0.4rem;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.md-overlay {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.md-footer {
|
||||
position: static;
|
||||
padding-left: 0;
|
||||
}
|
||||
.md-footer__link, .md-footer-nav__link {
|
||||
/* footer links begin to overlap at small sizes without setting width */
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
.md-sidebar--primary {
|
||||
left: -16rem !important;
|
||||
width: 16rem;
|
||||
}
|
||||
.md-sidebar--primary .md-sidebar__scrollwrap {
|
||||
bottom: ${sidebarConfig.mobileSidebarHeight}px;
|
||||
}
|
||||
}
|
||||
`,
|
||||
}),
|
||||
injectCss({
|
||||
// Typeset
|
||||
css: `
|
||||
.md-typeset {
|
||||
font-size: var(--md-typeset-font-size);
|
||||
}
|
||||
|
||||
${headings.reduce<string>((style, heading) => {
|
||||
const styles = theme.typography[heading];
|
||||
const { lineHeight, fontFamily, fontWeight, fontSize } = styles;
|
||||
const calculate = (value: typeof fontSize) => {
|
||||
let factor: number | string = 1;
|
||||
if (typeof value === 'number') {
|
||||
// 60% of the size defined because it is too big
|
||||
factor = (value / 16) * 0.6;
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
factor = value.replace('rem', '');
|
||||
}
|
||||
return `calc(${factor} * var(--md-typeset-font-size))`;
|
||||
};
|
||||
return style.concat(`
|
||||
.md-typeset ${heading} {
|
||||
color: var(--md-default-fg-color);
|
||||
line-height: ${lineHeight};
|
||||
font-family: ${fontFamily};
|
||||
font-weight: ${fontWeight};
|
||||
font-size: ${calculate(fontSize)};
|
||||
}
|
||||
`);
|
||||
}, '')}
|
||||
|
||||
.md-typeset .md-content__button {
|
||||
color: var(--md-default-fg-color);
|
||||
}
|
||||
|
||||
.md-typeset hr {
|
||||
border-bottom: 0.05rem dotted ${theme.palette.divider};
|
||||
}
|
||||
|
||||
.md-typeset details {
|
||||
font-size: var(--md-typeset-font-size) !important;
|
||||
}
|
||||
.md-typeset details summary {
|
||||
padding-left: 2.5rem !important;
|
||||
}
|
||||
.md-typeset details summary:before,
|
||||
.md-typeset details summary:after {
|
||||
top: 50% !important;
|
||||
width: 20px !important;
|
||||
height: 20px !important;
|
||||
transform: rotate(0deg) translateY(-50%) !important;
|
||||
}
|
||||
.md-typeset details[open] > summary:after {
|
||||
transform: rotate(90deg) translateX(-50%) !important;
|
||||
}
|
||||
|
||||
.md-typeset blockquote {
|
||||
color: var(--md-default-fg-color--light);
|
||||
border-left: 0.2rem solid var(--md-default-fg-color--light);
|
||||
}
|
||||
|
||||
.md-typeset table:not([class]) {
|
||||
font-size: var(--md-typeset-font-size);
|
||||
border: 1px solid var(--md-default-fg-color);
|
||||
border-bottom: none;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.md-typeset table:not([class]) th {
|
||||
font-weight: bold;
|
||||
}
|
||||
.md-typeset table:not([class]) td, .md-typeset table:not([class]) th {
|
||||
border-bottom: 1px solid var(--md-default-fg-color);
|
||||
}
|
||||
|
||||
.md-typeset pre > code::-webkit-scrollbar-thumb {
|
||||
background-color: hsla(0, 0%, 0%, 0.32);
|
||||
}
|
||||
.md-typeset pre > code::-webkit-scrollbar-thumb:hover {
|
||||
background-color: hsla(0, 0%, 0%, 0.87);
|
||||
}
|
||||
`,
|
||||
}),
|
||||
injectCss({
|
||||
// Animations
|
||||
css: `
|
||||
/*
|
||||
Disable CSS animations on link colors as they lead to issues in dark mode.
|
||||
The dark mode color theme is applied later and theirfore there is always an animation from light to dark mode when navigation between pages.
|
||||
*/
|
||||
.md-dialog, .md-nav__link, .md-footer__link, .md-typeset a, .md-typeset a::before, .md-typeset .headerlink {
|
||||
transition: none;
|
||||
}
|
||||
`,
|
||||
}),
|
||||
injectCss({
|
||||
// Extensions
|
||||
css: `
|
||||
/* HIGHLIGHT */
|
||||
.highlight .md-clipboard:after {
|
||||
content: unset;
|
||||
}
|
||||
|
||||
.highlight .nx {
|
||||
color: ${isDarkTheme ? '#ff53a3' : '#ec407a'};
|
||||
}
|
||||
|
||||
/* CODE HILITE */
|
||||
.codehilite .gd {
|
||||
background-color: ${
|
||||
isDarkTheme ? 'rgba(248,81,73,0.65)' : '#fdd'
|
||||
};
|
||||
}
|
||||
|
||||
.codehilite .gi {
|
||||
background-color: ${
|
||||
isDarkTheme ? 'rgba(46,160,67,0.65)' : '#dfd'
|
||||
};
|
||||
}
|
||||
|
||||
/* TABBED */
|
||||
.tabbed-set>input:nth-child(1):checked~.tabbed-labels>:nth-child(1),
|
||||
.tabbed-set>input:nth-child(2):checked~.tabbed-labels>:nth-child(2),
|
||||
.tabbed-set>input:nth-child(3):checked~.tabbed-labels>:nth-child(3),
|
||||
.tabbed-set>input:nth-child(4):checked~.tabbed-labels>:nth-child(4),
|
||||
.tabbed-set>input:nth-child(5):checked~.tabbed-labels>:nth-child(5),
|
||||
.tabbed-set>input:nth-child(6):checked~.tabbed-labels>:nth-child(6),
|
||||
.tabbed-set>input:nth-child(7):checked~.tabbed-labels>:nth-child(7),
|
||||
.tabbed-set>input:nth-child(8):checked~.tabbed-labels>:nth-child(8),
|
||||
.tabbed-set>input:nth-child(9):checked~.tabbed-labels>:nth-child(9),
|
||||
.tabbed-set>input:nth-child(10):checked~.tabbed-labels>:nth-child(10),
|
||||
.tabbed-set>input:nth-child(11):checked~.tabbed-labels>:nth-child(11),
|
||||
.tabbed-set>input:nth-child(12):checked~.tabbed-labels>:nth-child(12),
|
||||
.tabbed-set>input:nth-child(13):checked~.tabbed-labels>:nth-child(13),
|
||||
.tabbed-set>input:nth-child(14):checked~.tabbed-labels>:nth-child(14),
|
||||
.tabbed-set>input:nth-child(15):checked~.tabbed-labels>:nth-child(15),
|
||||
.tabbed-set>input:nth-child(16):checked~.tabbed-labels>:nth-child(16),
|
||||
.tabbed-set>input:nth-child(17):checked~.tabbed-labels>:nth-child(17),
|
||||
.tabbed-set>input:nth-child(18):checked~.tabbed-labels>:nth-child(18),
|
||||
.tabbed-set>input:nth-child(19):checked~.tabbed-labels>:nth-child(19),
|
||||
.tabbed-set>input:nth-child(20):checked~.tabbed-labels>:nth-child(20) {
|
||||
color: var(--md-accent-fg-color);
|
||||
border-color: var(--md-accent-fg-color);
|
||||
}
|
||||
|
||||
/* TASK-LIST */
|
||||
.task-list-control .task-list-indicator::before {
|
||||
background-color: ${theme.palette.action.disabledBackground};
|
||||
}
|
||||
.task-list-control [type="checkbox"]:checked + .task-list-indicator:before {
|
||||
background-color: ${theme.palette.success.main};
|
||||
}
|
||||
|
||||
/* ADMONITION */
|
||||
.admonition {
|
||||
font-size: var(--md-typeset-font-size) !important;
|
||||
}
|
||||
.admonition .admonition-title {
|
||||
padding-left: 2.5rem !important;
|
||||
}
|
||||
|
||||
.admonition .admonition-title:before {
|
||||
top: 50% !important;
|
||||
width: 20px !important;
|
||||
height: 20px !important;
|
||||
transform: translateY(-50%) !important;
|
||||
}
|
||||
`,
|
||||
}),
|
||||
]),
|
||||
[
|
||||
kind,
|
||||
name,
|
||||
namespace,
|
||||
scmIntegrationsApi,
|
||||
techdocsSanitizer,
|
||||
techdocsStorageApi,
|
||||
theme,
|
||||
isDarkTheme,
|
||||
isPinned,
|
||||
],
|
||||
);
|
||||
|
||||
// a function that performs transformations that are executed after adding it to the DOM
|
||||
const postRender = useCallback(
|
||||
async (transformedElement: Element) =>
|
||||
transformer(transformedElement, [
|
||||
scrollIntoAnchor(),
|
||||
copyToClipboard(theme),
|
||||
addLinkClickListener({
|
||||
baseUrl: window.location.origin,
|
||||
onClick: (event: MouseEvent, url: string) => {
|
||||
// detect if CTRL or META keys are pressed so that links can be opened in a new tab with `window.open`
|
||||
const modifierActive = event.ctrlKey || event.metaKey;
|
||||
const parsedUrl = new URL(url);
|
||||
|
||||
// hash exists when anchor is clicked on secondary sidebar
|
||||
if (parsedUrl.hash) {
|
||||
if (modifierActive) {
|
||||
window.open(`${parsedUrl.pathname}${parsedUrl.hash}`, '_blank');
|
||||
} else {
|
||||
navigate(`${parsedUrl.pathname}${parsedUrl.hash}`);
|
||||
// Scroll to hash if it's on the current page
|
||||
transformedElement
|
||||
?.querySelector(`[id='${parsedUrl.hash.slice(1)}']`)
|
||||
?.scrollIntoView();
|
||||
}
|
||||
} else {
|
||||
if (modifierActive) {
|
||||
window.open(parsedUrl.pathname, '_blank');
|
||||
} else {
|
||||
navigate(parsedUrl.pathname);
|
||||
// Scroll to top of reader if primary sidebar link is clicked
|
||||
transformedElement
|
||||
?.querySelector('.md-content__inner')
|
||||
?.scrollIntoView();
|
||||
}
|
||||
}
|
||||
},
|
||||
}),
|
||||
onCssReady({
|
||||
docStorageUrl: await techdocsStorageApi.getApiOrigin(),
|
||||
onLoading: (renderedElement: Element) => {
|
||||
(renderedElement as HTMLElement).style.setProperty('opacity', '0');
|
||||
},
|
||||
onLoaded: (renderedElement: Element) => {
|
||||
(renderedElement as HTMLElement).style.removeProperty('opacity');
|
||||
// disable MkDocs drawer toggling ('for' attribute => checkbox mechanism)
|
||||
renderedElement
|
||||
.querySelector('.md-nav__title')
|
||||
?.removeAttribute('for');
|
||||
setSidebars(
|
||||
Array.from(renderedElement.querySelectorAll('.md-sidebar')),
|
||||
);
|
||||
},
|
||||
}),
|
||||
]),
|
||||
[theme, navigate, techdocsStorageApi],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!rawPage) return () => {};
|
||||
|
||||
// if false, there is already a newer execution of this effect
|
||||
let shouldReplaceContent = true;
|
||||
|
||||
// Pre-render
|
||||
preRender(rawPage, path).then(async preTransformedDomElement => {
|
||||
if (!preTransformedDomElement?.innerHTML) {
|
||||
return; // An unexpected error occurred
|
||||
}
|
||||
|
||||
// don't manipulate the shadow dom if this isn't the latest effect execution
|
||||
if (!shouldReplaceContent) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Scroll to top after render
|
||||
window.scroll({ top: 0 });
|
||||
|
||||
// Post-render
|
||||
const postTransformedDomElement = await postRender(
|
||||
preTransformedDomElement,
|
||||
);
|
||||
setDom(postTransformedDomElement as HTMLElement);
|
||||
});
|
||||
|
||||
// cancel this execution
|
||||
return () => {
|
||||
shouldReplaceContent = false;
|
||||
};
|
||||
}, [rawPage, path, preRender, postRender]);
|
||||
|
||||
return dom;
|
||||
};
|
||||
|
||||
const TheReader = ({
|
||||
entityRef,
|
||||
onReady = () => {},
|
||||
withSearch = true,
|
||||
}: ReaderProps) => {
|
||||
const classes = useStyles();
|
||||
const dom = useTechDocsReaderDom(entityRef);
|
||||
const shadowDomRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const onReadyRef = useRef<() => void>(onReady);
|
||||
useEffect(() => {
|
||||
onReadyRef.current = onReady;
|
||||
}, [onReady]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!dom || !shadowDomRef.current) return;
|
||||
const shadowDiv = shadowDomRef.current;
|
||||
const shadowRoot =
|
||||
shadowDiv.shadowRoot || shadowDiv.attachShadow({ mode: 'open' });
|
||||
Array.from(shadowRoot.children).forEach(child =>
|
||||
shadowRoot.removeChild(child),
|
||||
);
|
||||
shadowRoot.appendChild(dom);
|
||||
onReadyRef.current();
|
||||
|
||||
// this hook must ONLY be triggered by a changed dom
|
||||
}, [dom]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<TechDocsStateIndicator />
|
||||
{withSearch && shadowDomRef?.current?.shadowRoot?.innerHTML && (
|
||||
<Grid container className={classes.searchBar}>
|
||||
<TechDocsSearch entityId={entityRef} />
|
||||
</Grid>
|
||||
)}
|
||||
<div data-testid="techdocs-content-shadowroot" ref={shadowDomRef} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Component responsible for rendering TechDocs documentation
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const Reader = (props: ReaderProps) => {
|
||||
const { entityRef, onReady = () => {}, withSearch = true } = props;
|
||||
return (
|
||||
<TechDocsReaderProvider entityRef={entityRef}>
|
||||
<TheReader
|
||||
entityRef={entityRef}
|
||||
onReady={onReady}
|
||||
withSearch={withSearch}
|
||||
/>
|
||||
</TechDocsReaderProvider>
|
||||
);
|
||||
};
|
||||
@@ -1,176 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 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 { TechDocsReaderPage } from './TechDocsReaderPage';
|
||||
import { render, act } from '@testing-library/react';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import {
|
||||
ScmIntegrationsApi,
|
||||
scmIntegrationsApiRef,
|
||||
} from '@backstage/integration-react';
|
||||
import { TestApiRegistry, wrapInTestApp } from '@backstage/test-utils';
|
||||
import { Header } from '@backstage/core-components';
|
||||
import {
|
||||
techdocsApiRef,
|
||||
TechDocsApi,
|
||||
techdocsStorageApiRef,
|
||||
TechDocsStorageApi,
|
||||
} from '../../api';
|
||||
import { ApiProvider } from '@backstage/core-app-api';
|
||||
import { searchApiRef } from '@backstage/plugin-search-react';
|
||||
|
||||
jest.mock('react-router-dom', () => {
|
||||
const actual = jest.requireActual('react-router-dom');
|
||||
return {
|
||||
...actual,
|
||||
useParams: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('./TechDocsReaderPageHeader', () => {
|
||||
return {
|
||||
__esModule: true,
|
||||
TechDocsReaderPageHeader: () => <div />,
|
||||
};
|
||||
});
|
||||
|
||||
const { useParams }: { useParams: jest.Mock } =
|
||||
jest.requireMock('react-router-dom');
|
||||
global.scroll = jest.fn();
|
||||
|
||||
describe('<TechDocsReaderPage />', () => {
|
||||
it('should render techdocs page', async () => {
|
||||
useParams.mockReturnValue({
|
||||
entityRef: 'Component::backstage',
|
||||
});
|
||||
|
||||
const scmIntegrationsApi: ScmIntegrationsApi =
|
||||
ScmIntegrationsApi.fromConfig(
|
||||
new ConfigReader({
|
||||
integrations: {},
|
||||
}),
|
||||
);
|
||||
const techdocsApi: Partial<TechDocsApi> = {
|
||||
getEntityMetadata: () =>
|
||||
Promise.resolve({
|
||||
apiVersion: 'v1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'backstage',
|
||||
},
|
||||
}),
|
||||
getTechDocsMetadata: () =>
|
||||
Promise.resolve({
|
||||
site_name: 'string',
|
||||
site_description: 'string',
|
||||
}),
|
||||
};
|
||||
|
||||
const techdocsStorageApi: Partial<TechDocsStorageApi> = {
|
||||
getEntityDocs: (): Promise<string> => Promise.resolve('String'),
|
||||
getBaseUrl: (): Promise<string> => Promise.resolve('String'),
|
||||
getApiOrigin: (): Promise<string> => Promise.resolve('String'),
|
||||
};
|
||||
const searchApi = {
|
||||
query: () =>
|
||||
Promise.resolve({
|
||||
results: [],
|
||||
}),
|
||||
};
|
||||
const apiRegistry = TestApiRegistry.from(
|
||||
[scmIntegrationsApiRef, scmIntegrationsApi],
|
||||
[techdocsApiRef, techdocsApi],
|
||||
[techdocsStorageApiRef, techdocsStorageApi],
|
||||
[searchApiRef, searchApi],
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
const rendered = render(
|
||||
wrapInTestApp(
|
||||
<ApiProvider apis={apiRegistry}>
|
||||
<TechDocsReaderPage />
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
expect(rendered.getByTestId('techdocs-content')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should render techdocs page with custom header', async () => {
|
||||
useParams.mockReturnValue({
|
||||
entityRef: 'Component::backstage',
|
||||
});
|
||||
|
||||
const scmIntegrationsApi: ScmIntegrationsApi =
|
||||
ScmIntegrationsApi.fromConfig(
|
||||
new ConfigReader({
|
||||
integrations: {},
|
||||
}),
|
||||
);
|
||||
const techdocsApi: Partial<TechDocsApi> = {
|
||||
getEntityMetadata: () =>
|
||||
Promise.resolve({
|
||||
apiVersion: 'v1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'backstage',
|
||||
},
|
||||
}),
|
||||
getTechDocsMetadata: () =>
|
||||
Promise.resolve({
|
||||
site_name: 'string',
|
||||
site_description: 'string',
|
||||
}),
|
||||
};
|
||||
|
||||
const techdocsStorageApi: Partial<TechDocsStorageApi> = {
|
||||
getEntityDocs: (): Promise<string> => Promise.resolve('String'),
|
||||
getBaseUrl: (): Promise<string> => Promise.resolve('String'),
|
||||
getApiOrigin: (): Promise<string> => Promise.resolve('String'),
|
||||
};
|
||||
const searchApi = {
|
||||
query: () =>
|
||||
Promise.resolve({
|
||||
results: [],
|
||||
}),
|
||||
};
|
||||
const apiRegistry = TestApiRegistry.from(
|
||||
[scmIntegrationsApiRef, scmIntegrationsApi],
|
||||
[techdocsApiRef, techdocsApi],
|
||||
[techdocsStorageApiRef, techdocsStorageApi],
|
||||
[searchApiRef, searchApi],
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
const rendered = render(
|
||||
wrapInTestApp(
|
||||
<ApiProvider apis={apiRegistry}>
|
||||
<TechDocsReaderPage>
|
||||
{({ techdocsMetadataValue }) => (
|
||||
<Header
|
||||
type="documentation"
|
||||
title="A custom header"
|
||||
subtitle={techdocsMetadataValue?.site_name}
|
||||
/>
|
||||
)}
|
||||
</TechDocsReaderPage>
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
expect(rendered.getByText('A custom header')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,122 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 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, { useCallback, useState } from 'react';
|
||||
import { useOutlet } from 'react-router';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import useAsync from 'react-use/lib/useAsync';
|
||||
import { Reader } from './Reader';
|
||||
import { TechDocsReaderPageHeader } from './TechDocsReaderPageHeader';
|
||||
import { techdocsApiRef } from '../../api';
|
||||
import { TechDocsEntityMetadata, TechDocsMetadata } from '../../types';
|
||||
import { CompoundEntityRef } from '@backstage/catalog-model';
|
||||
import { useApi, useApp } from '@backstage/core-plugin-api';
|
||||
import { Page, Content } from '@backstage/core-components';
|
||||
|
||||
/**
|
||||
* Helper function that gives the children of {@link TechDocsReaderPage} access to techdocs and entity metadata
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type TechDocsReaderPageRenderFunction = ({
|
||||
techdocsMetadataValue,
|
||||
entityMetadataValue,
|
||||
entityRef,
|
||||
}: {
|
||||
techdocsMetadataValue?: TechDocsMetadata | undefined;
|
||||
entityMetadataValue?: TechDocsEntityMetadata | undefined;
|
||||
entityRef: CompoundEntityRef;
|
||||
onReady: () => void;
|
||||
}) => JSX.Element;
|
||||
|
||||
/**
|
||||
* Props for {@link TechDocsReaderPage}
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type TechDocsReaderPageProps = {
|
||||
children?: TechDocsReaderPageRenderFunction | React.ReactNode;
|
||||
};
|
||||
|
||||
export const TechDocsReaderPage = (props: TechDocsReaderPageProps) => {
|
||||
const { children } = props;
|
||||
const { NotFoundErrorPage } = useApp().getComponents();
|
||||
const outlet = useOutlet();
|
||||
|
||||
const [documentReady, setDocumentReady] = useState<boolean>(false);
|
||||
const { namespace, kind, name } = useParams();
|
||||
|
||||
const techdocsApi = useApi(techdocsApiRef);
|
||||
|
||||
const { value: techdocsMetadataValue } = useAsync(() => {
|
||||
if (documentReady) {
|
||||
return techdocsApi.getTechDocsMetadata({ kind, namespace, name });
|
||||
}
|
||||
|
||||
return Promise.resolve(undefined);
|
||||
}, [kind, namespace, name, techdocsApi, documentReady]);
|
||||
|
||||
const { value: entityMetadataValue, error: entityMetadataError } =
|
||||
useAsync(() => {
|
||||
return techdocsApi.getEntityMetadata({ kind, namespace, name });
|
||||
}, [kind, namespace, name, techdocsApi]);
|
||||
|
||||
const onReady = useCallback(() => {
|
||||
setDocumentReady(true);
|
||||
}, [setDocumentReady]);
|
||||
|
||||
if (entityMetadataError) return <NotFoundErrorPage />;
|
||||
|
||||
if (!children)
|
||||
return (
|
||||
outlet || (
|
||||
<Page themeId="documentation">
|
||||
<TechDocsReaderPageHeader
|
||||
techDocsMetadata={techdocsMetadataValue}
|
||||
entityMetadata={entityMetadataValue}
|
||||
entityRef={{
|
||||
kind,
|
||||
namespace,
|
||||
name,
|
||||
}}
|
||||
/>
|
||||
<Content data-testid="techdocs-content">
|
||||
<Reader
|
||||
onReady={onReady}
|
||||
entityRef={{
|
||||
kind,
|
||||
namespace,
|
||||
name,
|
||||
}}
|
||||
/>
|
||||
</Content>
|
||||
</Page>
|
||||
)
|
||||
);
|
||||
|
||||
return (
|
||||
<Page themeId="documentation">
|
||||
{children instanceof Function
|
||||
? children({
|
||||
techdocsMetadataValue,
|
||||
entityMetadataValue,
|
||||
entityRef: { kind, namespace, name },
|
||||
onReady,
|
||||
})
|
||||
: children}
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
* Copyright 2020 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 { act } from '@testing-library/react';
|
||||
import { ThemeProvider } from '@material-ui/core';
|
||||
import { scmIntegrationsApiRef } from '@backstage/integration-react';
|
||||
|
||||
import { lightTheme } from '@backstage/theme';
|
||||
import { entityRouteRef } from '@backstage/plugin-catalog-react';
|
||||
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
|
||||
|
||||
import { techdocsApiRef, techdocsStorageApiRef } from '../../../api';
|
||||
|
||||
import { rootRouteRef, rootDocsRouteRef } from '../../../routes';
|
||||
|
||||
import { TechDocsReaderPage } from './TechDocsReaderPage';
|
||||
|
||||
const mockEntityMetadata = {
|
||||
locationMetadata: {
|
||||
type: 'github',
|
||||
target: 'https://example.com/',
|
||||
},
|
||||
apiVersion: 'v1',
|
||||
kind: 'test',
|
||||
metadata: {
|
||||
name: 'test-name',
|
||||
namespace: 'test-namespace',
|
||||
},
|
||||
spec: {
|
||||
owner: 'test',
|
||||
},
|
||||
};
|
||||
|
||||
const mockTechDocsMetadata = {
|
||||
site_name: 'test-site-name',
|
||||
site_description: 'test-site-desc',
|
||||
};
|
||||
|
||||
const getEntityMetadata = jest.fn();
|
||||
const getTechDocsMetadata = jest.fn();
|
||||
|
||||
const techdocsApiMock = {
|
||||
getEntityMetadata,
|
||||
getTechDocsMetadata,
|
||||
};
|
||||
|
||||
const techdocsStorageApiMock: jest.Mocked<typeof techdocsStorageApiRef.T> = {
|
||||
getApiOrigin: jest.fn(),
|
||||
getBaseUrl: jest.fn(),
|
||||
getBuilder: jest.fn(),
|
||||
getEntityDocs: jest.fn(),
|
||||
getStorageUrl: jest.fn(),
|
||||
syncEntityDocs: jest.fn(),
|
||||
};
|
||||
|
||||
const Wrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[scmIntegrationsApiRef, {}],
|
||||
[techdocsApiRef, techdocsApiMock],
|
||||
[techdocsStorageApiRef, techdocsStorageApiMock],
|
||||
]}
|
||||
>
|
||||
{children}
|
||||
</TestApiProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const mountedRoutes = {
|
||||
'/catalog/:namespace/:kind/:name/*': entityRouteRef,
|
||||
'/docs': rootRouteRef,
|
||||
'/docs/:namespace/:kind/:name/*': rootDocsRouteRef,
|
||||
};
|
||||
|
||||
describe('<TechDocsReaderPage />', () => {
|
||||
beforeEach(() => {
|
||||
getEntityMetadata.mockResolvedValue(mockEntityMetadata);
|
||||
getTechDocsMetadata.mockResolvedValue(mockTechDocsMetadata);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
it('should render a techdocs reader page without children', async () => {
|
||||
const rendered = await renderInTestApp(
|
||||
<Wrapper>
|
||||
<TechDocsReaderPage
|
||||
entityRef={{
|
||||
name: 'test-name',
|
||||
namespace: 'test-namespace',
|
||||
kind: 'test',
|
||||
}}
|
||||
/>
|
||||
</Wrapper>,
|
||||
{
|
||||
mountedRoutes,
|
||||
},
|
||||
);
|
||||
|
||||
// TechDocsReaderPageHeader
|
||||
expect(rendered.container.querySelector('header')).toBeInTheDocument();
|
||||
// TechDocsReaderPageContent
|
||||
expect(rendered.container.querySelector('article')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render a techdocs reader page with children', async () => {
|
||||
await act(async () => {
|
||||
const rendered = await renderInTestApp(
|
||||
<Wrapper>
|
||||
<TechDocsReaderPage
|
||||
entityRef={{
|
||||
name: 'test-name',
|
||||
namespace: 'test-namespace',
|
||||
kind: 'test',
|
||||
}}
|
||||
>
|
||||
techdocs reader page
|
||||
</TechDocsReaderPage>
|
||||
</Wrapper>,
|
||||
{
|
||||
mountedRoutes,
|
||||
},
|
||||
);
|
||||
expect(
|
||||
rendered.container.querySelector('header'),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
rendered.container.querySelector('article'),
|
||||
).not.toBeInTheDocument();
|
||||
expect(rendered.getByText('techdocs reader page')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* 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, { ReactNode, ReactChild, Children } from 'react';
|
||||
import { useOutlet, useParams } from 'react-router-dom';
|
||||
|
||||
import { Page } from '@backstage/core-components';
|
||||
import { CompoundEntityRef } from '@backstage/catalog-model';
|
||||
import {
|
||||
TECHDOCS_ADDONS_WRAPPER_KEY,
|
||||
TechDocsReaderPageProvider,
|
||||
} from '@backstage/plugin-techdocs-react';
|
||||
|
||||
import { TechDocsReaderPageRenderFunction } from '../../../types';
|
||||
|
||||
import { TechDocsReaderPageContent } from '../TechDocsReaderPageContent';
|
||||
import { TechDocsReaderPageHeader } from '../TechDocsReaderPageHeader';
|
||||
import { TechDocsReaderPageSubheader } from '../TechDocsReaderPageSubheader';
|
||||
|
||||
type Extension = ReactChild & {
|
||||
type: {
|
||||
__backstage_data: {
|
||||
map: Map<string, boolean>;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Props for {@link TechDocsReaderLayout}
|
||||
* @public
|
||||
*/
|
||||
export type TechDocsReaderLayoutProps = {
|
||||
/**
|
||||
* Show or hide the header, defaults to true.
|
||||
*/
|
||||
withHeader?: boolean;
|
||||
/**
|
||||
* Show or hide the content search bar, defaults to true.
|
||||
*/
|
||||
withSearch?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Default TechDocs reader page structure composed with a header and content
|
||||
* @public
|
||||
*/
|
||||
export const TechDocsReaderLayout = ({
|
||||
withSearch,
|
||||
withHeader = true,
|
||||
}: TechDocsReaderLayoutProps) => {
|
||||
return (
|
||||
<Page themeId="documentation">
|
||||
{withHeader && <TechDocsReaderPageHeader />}
|
||||
<TechDocsReaderPageSubheader />
|
||||
<TechDocsReaderPageContent withSearch={withSearch} />
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type TechDocsReaderPageProps = {
|
||||
entityRef?: CompoundEntityRef;
|
||||
children?: TechDocsReaderPageRenderFunction | ReactNode;
|
||||
};
|
||||
|
||||
/**
|
||||
* An addon-aware implementation of the TechDocsReaderPage.
|
||||
* @public
|
||||
*/
|
||||
export const TechDocsReaderPage = (props: TechDocsReaderPageProps) => {
|
||||
const { kind, name, namespace } = useParams();
|
||||
const { children, entityRef = { kind, name, namespace } } = props;
|
||||
|
||||
const outlet = useOutlet();
|
||||
|
||||
if (!children) {
|
||||
const childrenList = outlet ? Children.toArray(outlet.props.children) : [];
|
||||
|
||||
const page = childrenList.find(child => {
|
||||
const { type } = child as Extension;
|
||||
return !type?.__backstage_data?.map?.get(TECHDOCS_ADDONS_WRAPPER_KEY);
|
||||
});
|
||||
|
||||
return (
|
||||
(page as JSX.Element) || (
|
||||
<TechDocsReaderPageProvider entityRef={entityRef}>
|
||||
<TechDocsReaderLayout />
|
||||
</TechDocsReaderPageProvider>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TechDocsReaderPageProvider entityRef={entityRef}>
|
||||
{({ metadata, entityMetadata, onReady }) => (
|
||||
<Page themeId="documentation">
|
||||
{children instanceof Function
|
||||
? children({
|
||||
entityRef,
|
||||
techdocsMetadataValue: metadata.value,
|
||||
entityMetadataValue: entityMetadata.value,
|
||||
onReady,
|
||||
})
|
||||
: children}
|
||||
</Page>
|
||||
)}
|
||||
</TechDocsReaderPageProvider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* 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 { renderHook } from '@testing-library/react-hooks';
|
||||
|
||||
import { ThemeProvider } from '@material-ui/core';
|
||||
|
||||
import { lightTheme } from '@backstage/theme';
|
||||
import { TestApiProvider } from '@backstage/test-utils';
|
||||
import { Entity, CompoundEntityRef } from '@backstage/catalog-model';
|
||||
import {
|
||||
techdocsApiRef,
|
||||
TechDocsMetadata,
|
||||
TechDocsReaderPageProvider,
|
||||
} from '@backstage/plugin-techdocs-react';
|
||||
|
||||
import { useEntityMetadata, useTechDocsMetadata } from './context';
|
||||
|
||||
const mockEntityMetadata: Entity = {
|
||||
apiVersion: 'v1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'test',
|
||||
namespace: 'default',
|
||||
},
|
||||
spec: {
|
||||
owner: 'test',
|
||||
},
|
||||
};
|
||||
|
||||
const mockTechDocsMetadata: TechDocsMetadata = {
|
||||
site_name: 'test-componnet',
|
||||
site_description: 'this is a test component',
|
||||
};
|
||||
|
||||
const techdocsApiMock = {
|
||||
getEntityMetadata: jest.fn().mockResolvedValue(mockEntityMetadata),
|
||||
getTechDocsMetadata: jest.fn().mockResolvedValue(mockTechDocsMetadata),
|
||||
};
|
||||
|
||||
const wrapper = ({
|
||||
entityRef = {
|
||||
kind: mockEntityMetadata.kind,
|
||||
name: mockEntityMetadata.metadata.name,
|
||||
namespace: mockEntityMetadata.metadata.namespace!!,
|
||||
},
|
||||
children,
|
||||
}: {
|
||||
entityRef?: CompoundEntityRef;
|
||||
children: React.ReactNode;
|
||||
}) => (
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<TestApiProvider apis={[[techdocsApiRef, techdocsApiMock]]}>
|
||||
<TechDocsReaderPageProvider entityRef={entityRef}>
|
||||
{children}
|
||||
</TechDocsReaderPageProvider>
|
||||
</TestApiProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
describe('context', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
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, waitForNextUpdate } = renderHook(
|
||||
() => useEntityMetadata(),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(result.current.value).toBeDefined();
|
||||
expect(result.current.error).toBeUndefined();
|
||||
expect(result.current.value).toMatchObject(mockEntityMetadata);
|
||||
});
|
||||
});
|
||||
|
||||
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, waitForNextUpdate } = renderHook(
|
||||
() => useTechDocsMetadata(),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(result.current.value).toBeDefined();
|
||||
expect(result.current.error).toBeUndefined();
|
||||
expect(result.current.value).toMatchObject(mockTechDocsMetadata);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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 '@backstage/plugin-techdocs-react';
|
||||
|
||||
/**
|
||||
* Hook for sub-components to retrieve Entity Metadata for the current TechDocs
|
||||
* site.
|
||||
* @internal
|
||||
*/
|
||||
export const useEntityMetadata = () => {
|
||||
const { entityMetadata } = useTechDocsReaderPage();
|
||||
return entityMetadata;
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook for sub-components to retrieve TechDocs Metadata for the current
|
||||
* TechDocs site.
|
||||
* @internal
|
||||
*/
|
||||
export const useTechDocsMetadata = () => {
|
||||
const { metadata } = useTechDocsReaderPage();
|
||||
return metadata;
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* 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, TechDocsReaderLayout } from './TechDocsReaderPage';
|
||||
export type {
|
||||
TechDocsReaderPageProps,
|
||||
TechDocsReaderLayoutProps,
|
||||
} from './TechDocsReaderPage';
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
/*
|
||||
* 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, { useState, useCallback } from 'react';
|
||||
import { create } from 'jss';
|
||||
|
||||
import { makeStyles, Grid, Portal } from '@material-ui/core';
|
||||
import { StylesProvider, jssPreset } from '@material-ui/styles';
|
||||
|
||||
import {
|
||||
useTechDocsAddons,
|
||||
TechDocsAddonLocations as locations,
|
||||
useTechDocsReaderPage,
|
||||
} from '@backstage/plugin-techdocs-react';
|
||||
import { CompoundEntityRef } from '@backstage/catalog-model';
|
||||
import { Content, Progress } from '@backstage/core-components';
|
||||
|
||||
import { TechDocsSearch } from '../../../search';
|
||||
import { TechDocsStateIndicator } from '../TechDocsStateIndicator';
|
||||
|
||||
import { useTechDocsReaderDom } from './dom';
|
||||
import { withTechDocsReaderProvider } from './context';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
search: {
|
||||
width: '100%',
|
||||
'@media (min-width: 76.1875em)': {
|
||||
width: 'calc(100% - 34.4rem)',
|
||||
margin: '0 auto',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Props for {@link TechDocsReaderPageContent}
|
||||
* @public
|
||||
*/
|
||||
export type TechDocsReaderPageContentProps = {
|
||||
/**
|
||||
* @deprecated No need to pass down entityRef as property anymore. Consumes the entityName from `TechDocsReaderPageContext`. Use the {@link @backstage/plugin-techdocs-react#useTechDocsReaderPage} hook for custom reader page content.
|
||||
*/
|
||||
entityRef?: CompoundEntityRef;
|
||||
/**
|
||||
* Show or hide the search bar, defaults to true.
|
||||
*/
|
||||
withSearch?: boolean;
|
||||
/**
|
||||
* Callback called when the content is rendered.
|
||||
*/
|
||||
onReady?: () => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders the reader page content
|
||||
* @public
|
||||
*/
|
||||
export const TechDocsReaderPageContent = withTechDocsReaderProvider(
|
||||
(props: TechDocsReaderPageContentProps) => {
|
||||
const { withSearch = true, onReady } = props;
|
||||
const classes = useStyles();
|
||||
const addons = useTechDocsAddons();
|
||||
const { entityRef, shadowRoot, setShadowRoot } = useTechDocsReaderPage();
|
||||
const dom = useTechDocsReaderDom(entityRef);
|
||||
|
||||
const [jss, setJss] = useState(
|
||||
create({
|
||||
...jssPreset(),
|
||||
insertionPoint: undefined,
|
||||
}),
|
||||
);
|
||||
|
||||
const ref = useCallback(
|
||||
(shadowHost: HTMLDivElement) => {
|
||||
if (!dom || !shadowHost) return;
|
||||
|
||||
setJss(
|
||||
create({
|
||||
...jssPreset(),
|
||||
insertionPoint: dom.querySelector('head') || undefined,
|
||||
}),
|
||||
);
|
||||
|
||||
const newShadowRoot =
|
||||
shadowHost.shadowRoot ?? shadowHost.attachShadow({ mode: 'open' });
|
||||
newShadowRoot.innerHTML = '';
|
||||
newShadowRoot.appendChild(dom);
|
||||
setShadowRoot(newShadowRoot);
|
||||
if (onReady instanceof Function) {
|
||||
onReady();
|
||||
}
|
||||
},
|
||||
[dom, setShadowRoot, onReady],
|
||||
);
|
||||
|
||||
const contentElement = shadowRoot?.querySelector(
|
||||
'[data-md-component="content"]',
|
||||
);
|
||||
const primarySidebarElement = shadowRoot?.querySelector(
|
||||
'div[data-md-component="sidebar"][data-md-type="navigation"], div[data-md-component="navigation"]',
|
||||
);
|
||||
const secondarySidebarElement = 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>
|
||||
<Grid container>
|
||||
<Grid xs={12} item>
|
||||
<TechDocsStateIndicator />
|
||||
</Grid>
|
||||
{withSearch && (
|
||||
<Grid className={classes.search} xs="auto" item>
|
||||
<TechDocsSearch entityId={entityRef} />
|
||||
</Grid>
|
||||
)}
|
||||
<Grid xs={12} item>
|
||||
{/* 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.PrimarySidebar)}
|
||||
</Portal>
|
||||
<Portal container={contentElement}>
|
||||
{addons.renderComponentsByLocation(locations.Content)}
|
||||
</Portal>
|
||||
<Portal container={secondarySidebarAddonLocation}>
|
||||
{addons.renderComponentsByLocation(locations.SecondarySidebar)}
|
||||
</Portal>
|
||||
</StylesProvider>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Content>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* Props for {@link Reader}
|
||||
*
|
||||
* @public
|
||||
* @deprecated use `TechDocsReaderPageContentProps` instead.
|
||||
*/
|
||||
export type ReaderProps = TechDocsReaderPageContentProps;
|
||||
|
||||
/**
|
||||
* Component responsible for rendering TechDocs documentation
|
||||
* @public
|
||||
* @deprecated use `TechDocsReaderPageContent` component instead.
|
||||
*/
|
||||
export const Reader = TechDocsReaderPageContent;
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {
|
||||
ComponentType,
|
||||
createContext,
|
||||
useContext,
|
||||
ReactNode,
|
||||
} from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useTechDocsReaderPage } from '@backstage/plugin-techdocs-react';
|
||||
|
||||
import { useReaderState, ReaderState } from '../useReaderState';
|
||||
|
||||
const TechDocsReaderContext = createContext<ReaderState>({} as ReaderState);
|
||||
|
||||
/**
|
||||
* Note: this hook is currently being exported so that we can rapidly
|
||||
* iterate on alternative <Reader /> implementations that extend core
|
||||
* functionality. There is no guarantee that this hook will continue to be
|
||||
* exported by the package in the future!
|
||||
*
|
||||
* todo: Make public or stop exporting (ctrl+f "altReaderExperiments")
|
||||
* @internal
|
||||
*/
|
||||
|
||||
export const useTechDocsReader = () => useContext(TechDocsReaderContext);
|
||||
|
||||
/**
|
||||
* @public Render function for {@link TechDocsReaderProvider}
|
||||
*/
|
||||
export type TechDocsReaderProviderRenderFunction = (
|
||||
value: ReaderState,
|
||||
) => JSX.Element;
|
||||
|
||||
/**
|
||||
* @public Props for {@link TechDocsReaderProvider}
|
||||
*/
|
||||
export type TechDocsReaderProviderProps = {
|
||||
children: TechDocsReaderProviderRenderFunction | ReactNode;
|
||||
};
|
||||
|
||||
/**
|
||||
* Provides shared building process state to the reader page components.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const TechDocsReaderProvider = ({
|
||||
children,
|
||||
}: TechDocsReaderProviderProps) => {
|
||||
const { '*': path = '' } = useParams();
|
||||
const { entityRef } = useTechDocsReaderPage();
|
||||
const { kind, namespace, name } = entityRef;
|
||||
const value = useReaderState(kind, namespace, name, path);
|
||||
|
||||
return (
|
||||
<TechDocsReaderContext.Provider value={value}>
|
||||
{children instanceof Function ? children(value) : children}
|
||||
</TechDocsReaderContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Note: this HOC is currently being exported so that we can rapidly
|
||||
* iterate on alternative <Reader /> implementations that extend core
|
||||
* functionality. There is no guarantee that this HOC will continue to be
|
||||
* exported by the package in the future!
|
||||
*
|
||||
* todo: Make public or stop exporting (ctrl+f "altReaderExperiments")
|
||||
* @internal
|
||||
*/
|
||||
export const withTechDocsReaderProvider =
|
||||
<T extends {}>(Component: ComponentType<T>) =>
|
||||
(props: T) =>
|
||||
(
|
||||
<TechDocsReaderProvider>
|
||||
<Component {...props} />
|
||||
</TechDocsReaderProvider>
|
||||
);
|
||||
@@ -0,0 +1,786 @@
|
||||
/*
|
||||
* 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 { useContext, useCallback, useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { useTheme, Theme } from '@material-ui/core';
|
||||
import { lighten, alpha } from '@material-ui/core/styles';
|
||||
|
||||
import { BackstageTheme } from '@backstage/theme';
|
||||
import { CompoundEntityRef } from '@backstage/catalog-model';
|
||||
import { useApi, configApiRef } from '@backstage/core-plugin-api';
|
||||
import { SidebarPinStateContext } from '@backstage/core-components';
|
||||
import { scmIntegrationsApiRef } from '@backstage/integration-react';
|
||||
|
||||
import { techdocsStorageApiRef } from '../../../api';
|
||||
|
||||
import { useTechDocsReader } from './context';
|
||||
|
||||
import {
|
||||
addBaseUrl,
|
||||
addGitFeedbackLink,
|
||||
addLinkClickListener,
|
||||
addSidebarToggle,
|
||||
injectCss,
|
||||
onCssReady,
|
||||
removeMkdocsHeader,
|
||||
rewriteDocLinks,
|
||||
sanitizeDOM,
|
||||
simplifyMkdocsFooter,
|
||||
scrollIntoAnchor,
|
||||
transform as transformer,
|
||||
copyToClipboard,
|
||||
} from '../../transformers';
|
||||
|
||||
type TypographyHeadings = Pick<
|
||||
Theme['typography'],
|
||||
'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'
|
||||
>;
|
||||
type TypographyHeadingsKeys = keyof TypographyHeadings;
|
||||
|
||||
const headings: TypographyHeadingsKeys[] = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
|
||||
|
||||
/**
|
||||
* Hook that encapsulates the behavior of getting raw HTML and applying
|
||||
* transforms to it in order to make it function at a basic level in the
|
||||
* Backstage UI.
|
||||
*
|
||||
* Note: this hook is currently being exported so that we can rapidly iterate
|
||||
* on alternative <Reader /> implementations that extend core functionality.
|
||||
* There is no guarantee that this hook will continue to be exported by the
|
||||
* package in the future!
|
||||
*
|
||||
* todo: Make public or stop exporting (see others: "altReaderExperiments")
|
||||
* @internal
|
||||
*/
|
||||
export const useTechDocsReaderDom = (
|
||||
entityRef: CompoundEntityRef,
|
||||
): Element | null => {
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme<BackstageTheme>();
|
||||
const techdocsStorageApi = useApi(techdocsStorageApiRef);
|
||||
const scmIntegrationsApi = useApi(scmIntegrationsApiRef);
|
||||
const techdocsSanitizer = useApi(configApiRef);
|
||||
const { namespace, kind, name } = entityRef;
|
||||
const { state, path, content: rawPage } = useTechDocsReader();
|
||||
const isDarkTheme = theme.palette.type === 'dark';
|
||||
|
||||
const [sidebars, setSidebars] = useState<HTMLElement[]>();
|
||||
const [dom, setDom] = useState<HTMLElement | null>(null);
|
||||
|
||||
// sidebar pinned status to be used in computing CSS style injections
|
||||
const { isPinned } = useContext(SidebarPinStateContext);
|
||||
|
||||
const updateSidebarPosition = useCallback(() => {
|
||||
if (!dom || !sidebars) return;
|
||||
// set sidebar height so they don't initially render in wrong position
|
||||
const mdTabs = dom.querySelector('.md-container > .md-tabs');
|
||||
const sidebarsCollapsed = window.matchMedia(
|
||||
'screen and (max-width: 76.1875em)',
|
||||
).matches;
|
||||
const newTop = Math.max(dom.getBoundingClientRect().top, 0);
|
||||
sidebars.forEach(sidebar => {
|
||||
if (sidebarsCollapsed) {
|
||||
sidebar.style.top = '0px';
|
||||
} else if (mdTabs) {
|
||||
sidebar.style.top = `${
|
||||
newTop + mdTabs.getBoundingClientRect().height
|
||||
}px`;
|
||||
} else {
|
||||
sidebar.style.top = `${newTop}px`;
|
||||
}
|
||||
});
|
||||
}, [dom, sidebars]);
|
||||
|
||||
useEffect(() => {
|
||||
updateSidebarPosition();
|
||||
window.addEventListener('scroll', updateSidebarPosition, true);
|
||||
window.addEventListener('resize', updateSidebarPosition);
|
||||
return () => {
|
||||
window.removeEventListener('scroll', updateSidebarPosition, true);
|
||||
window.removeEventListener('resize', updateSidebarPosition);
|
||||
};
|
||||
// an update to "state" might lead to an updated UI so we include it as a trigger
|
||||
}, [updateSidebarPosition, state]);
|
||||
|
||||
// dynamically set width of footer to accommodate for pinning of the sidebar
|
||||
const updateFooterWidth = useCallback(() => {
|
||||
if (!dom) return;
|
||||
const footer = dom.querySelector('.md-footer') as HTMLElement;
|
||||
if (footer) {
|
||||
footer.style.width = `${dom.getBoundingClientRect().width}px`;
|
||||
}
|
||||
}, [dom]);
|
||||
|
||||
useEffect(() => {
|
||||
updateFooterWidth();
|
||||
window.addEventListener('resize', updateFooterWidth);
|
||||
return () => {
|
||||
window.removeEventListener('resize', updateFooterWidth);
|
||||
};
|
||||
});
|
||||
|
||||
// a function that performs transformations that are executed prior to adding it to the DOM
|
||||
const preRender = useCallback(
|
||||
(rawContent: string, contentPath: string) =>
|
||||
transformer(rawContent, [
|
||||
sanitizeDOM(techdocsSanitizer.getOptionalConfig('techdocs.sanitizer')),
|
||||
addBaseUrl({
|
||||
techdocsStorageApi,
|
||||
entityId: {
|
||||
kind,
|
||||
name,
|
||||
namespace,
|
||||
},
|
||||
path: contentPath,
|
||||
}),
|
||||
rewriteDocLinks(),
|
||||
addSidebarToggle(),
|
||||
removeMkdocsHeader(),
|
||||
simplifyMkdocsFooter(),
|
||||
addGitFeedbackLink(scmIntegrationsApi),
|
||||
injectCss({
|
||||
// Variables
|
||||
css: `
|
||||
/*
|
||||
As the MkDocs output is rendered in shadow DOM, the CSS variable definitions on the root selector are not applied. Instead, they have to be applied on :host.
|
||||
As there is no way to transform the served main*.css yet (for example in the backend), we have to copy from main*.css and modify them.
|
||||
*/
|
||||
:host {
|
||||
/* FONT */
|
||||
--md-default-fg-color: ${theme.palette.text.primary};
|
||||
--md-default-fg-color--light: ${theme.palette.text.secondary};
|
||||
--md-default-fg-color--lighter: ${lighten(
|
||||
theme.palette.text.secondary,
|
||||
0.7,
|
||||
)};
|
||||
--md-default-fg-color--lightest: ${lighten(
|
||||
theme.palette.text.secondary,
|
||||
0.3,
|
||||
)};
|
||||
|
||||
/* BACKGROUND */
|
||||
--md-default-bg-color:${theme.palette.background.default};
|
||||
--md-default-bg-color--light: ${theme.palette.background.paper};
|
||||
--md-default-bg-color--lighter: ${lighten(
|
||||
theme.palette.background.paper,
|
||||
0.7,
|
||||
)};
|
||||
--md-default-bg-color--lightest: ${lighten(
|
||||
theme.palette.background.paper,
|
||||
0.3,
|
||||
)};
|
||||
|
||||
/* PRIMARY */
|
||||
--md-primary-fg-color: ${theme.palette.primary.main};
|
||||
--md-primary-fg-color--light: ${theme.palette.primary.light};
|
||||
--md-primary-fg-color--dark: ${theme.palette.primary.dark};
|
||||
--md-primary-bg-color: ${theme.palette.primary.contrastText};
|
||||
--md-primary-bg-color--light: ${lighten(
|
||||
theme.palette.primary.contrastText,
|
||||
0.7,
|
||||
)};
|
||||
|
||||
/* ACCENT */
|
||||
--md-accent-fg-color: var(--md-primary-fg-color);
|
||||
|
||||
/* SHADOW */
|
||||
--md-shadow-z1: ${theme.shadows[1]};
|
||||
--md-shadow-z2: ${theme.shadows[2]};
|
||||
--md-shadow-z3: ${theme.shadows[3]};
|
||||
|
||||
/* EXTENSIONS */
|
||||
--md-admonition-fg-color: var(--md-default-fg-color);
|
||||
--md-admonition-bg-color: var(--md-default-bg-color);
|
||||
/* Admonitions and others are using SVG masks to define icons. These masks are defined as CSS variables. */
|
||||
--md-admonition-icon--note: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20.71 7.04c.39-.39.39-1.04 0-1.41l-2.34-2.34c-.37-.39-1.02-.39-1.41 0l-1.84 1.83 3.75 3.75M3 17.25V21h3.75L17.81 9.93l-3.75-3.75L3 17.25z"/></svg>');
|
||||
--md-admonition-icon--abstract: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M4 5h16v2H4V5m0 4h16v2H4V9m0 4h16v2H4v-2m0 4h10v2H4v-2z"/></svg>');
|
||||
--md-admonition-icon--info: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13 9h-2V7h2m0 10h-2v-6h2m-1-9A10 10 0 002 12a10 10 0 0010 10 10 10 0 0010-10A10 10 0 0012 2z"/></svg>');
|
||||
--md-admonition-icon--tip: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M17.55 11.2c-.23-.3-.5-.56-.76-.82-.65-.6-1.4-1.03-2.03-1.66C13.3 7.26 13 4.85 13.91 3c-.91.23-1.75.75-2.45 1.32-2.54 2.08-3.54 5.75-2.34 8.9.04.1.08.2.08.33 0 .22-.15.42-.35.5-.22.1-.46.04-.64-.12a.83.83 0 01-.15-.17c-1.1-1.43-1.28-3.48-.53-5.12C5.89 10 5 12.3 5.14 14.47c.04.5.1 1 .27 1.5.14.6.4 1.2.72 1.73 1.04 1.73 2.87 2.97 4.84 3.22 2.1.27 4.35-.12 5.96-1.6 1.8-1.66 2.45-4.32 1.5-6.6l-.13-.26c-.2-.46-.47-.87-.8-1.25l.05-.01m-3.1 6.3c-.28.24-.73.5-1.08.6-1.1.4-2.2-.16-2.87-.82 1.19-.28 1.89-1.16 2.09-2.05.17-.8-.14-1.46-.27-2.23-.12-.74-.1-1.37.18-2.06.17.38.37.76.6 1.06.76 1 1.95 1.44 2.2 2.8.04.14.06.28.06.43.03.82-.32 1.72-.92 2.27h.01z"/></svg>');
|
||||
--md-admonition-icon--success: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2m-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>');
|
||||
--md-admonition-icon--question: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M15.07 11.25l-.9.92C13.45 12.89 13 13.5 13 15h-2v-.5c0-1.11.45-2.11 1.17-2.83l1.24-1.26c.37-.36.59-.86.59-1.41a2 2 0 00-2-2 2 2 0 00-2 2H8a4 4 0 014-4 4 4 0 014 4 3.2 3.2 0 01-.93 2.25M13 19h-2v-2h2M12 2A10 10 0 002 12a10 10 0 0010 10 10 10 0 0010-10c0-5.53-4.5-10-10-10z"/></svg>');
|
||||
--md-admonition-icon--warning: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13 14h-2v-4h2m0 8h-2v-2h2M1 21h22L12 2 1 21z"/></svg>');
|
||||
--md-admonition-icon--failure: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 2c5.53 0 10 4.47 10 10s-4.47 10-10 10S2 17.53 2 12 6.47 2 12 2m3.59 5L12 10.59 8.41 7 7 8.41 10.59 12 7 15.59 8.41 17 12 13.41 15.59 17 17 15.59 13.41 12 17 8.41 15.59 7z"/></svg>');
|
||||
--md-admonition-icon--danger: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M11.5 20l4.86-9.73H13V4l-5 9.73h3.5V20M12 2c2.75 0 5.1 1 7.05 2.95C21 6.9 22 9.25 22 12s-1 5.1-2.95 7.05C17.1 21 14.75 22 12 22s-5.1-1-7.05-2.95C3 17.1 2 14.75 2 12s1-5.1 2.95-7.05C6.9 3 9.25 2 12 2z"/></svg>');
|
||||
--md-admonition-icon--bug: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M14 12h-4v-2h4m0 6h-4v-2h4m6-6h-2.81a5.985 5.985 0 00-1.82-1.96L17 4.41 15.59 3l-2.17 2.17a6.002 6.002 0 00-2.83 0L8.41 3 7 4.41l1.62 1.63C7.88 6.55 7.26 7.22 6.81 8H4v2h2.09c-.05.33-.09.66-.09 1v1H4v2h2v1c0 .34.04.67.09 1H4v2h2.81c1.04 1.79 2.97 3 5.19 3s4.15-1.21 5.19-3H20v-2h-2.09c.05-.33.09-.66.09-1v-1h2v-2h-2v-1c0-.34-.04-.67-.09-1H20V8z"/></svg>');
|
||||
--md-admonition-icon--example: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M7 13v-2h14v2H7m0 6v-2h14v2H7M7 7V5h14v2H7M3 8V5H2V4h2v4H3m-1 9v-1h3v4H2v-1h2v-.5H3v-1h1V17H2m2.25-7a.75.75 0 01.75.75c0 .2-.08.39-.21.52L3.12 13H5v1H2v-.92L4 11H2v-1h2.25z"/></svg>');
|
||||
--md-admonition-icon--quote: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M14 17h3l2-4V7h-6v6h3M6 17h3l2-4V7H5v6h3l-2 4z"/></svg>');
|
||||
--md-footnotes-icon: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19 7v4H5.83l3.58-3.59L8 6l-6 6 6 6 1.41-1.42L5.83 13H21V7h-2z"/></svg>');
|
||||
--md-details-icon: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M8.59 16.58 13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.42z"/></svg>');
|
||||
--md-tasklist-icon: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 5v14H5V5h14m0-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"/></svg>');
|
||||
--md-tasklist-icon--checked: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.11 0 2-.9 2-2V5c0-1.1-.89-2-2-2zm-9 14l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>');
|
||||
--md-nav-icon--prev: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 11v2H8l5.5 5.5-1.42 1.42L4.16 12l7.92-7.92L13.5 5.5 8 11h12z"/></svg>');
|
||||
--md-nav-icon--next: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M8.59 16.58 13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.42z"/></svg>');
|
||||
--md-toc-icon: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3 9h14V7H3v2m0 4h14v-2H3v2m0 4h14v-2H3v2m16 0h2v-2h-2v2m0-10v2h2V7h-2m0 6h2v-2h-2v2z"/></svg>');
|
||||
--md-clipboard-icon: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19 21H8V7h11m0-2H8a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h11a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2m-3-4H4a2 2 0 0 0-2 2v14h2V3h12V1z"/></svg>');
|
||||
--md-search-result-icon: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h7c-.41-.25-.8-.56-1.14-.9-.33-.33-.61-.7-.86-1.1H6V4h7v5h5v1.18c.71.16 1.39.43 2 .82V8l-6-6m6.31 16.9c1.33-2.11.69-4.9-1.4-6.22-2.11-1.33-4.91-.68-6.22 1.4-1.34 2.11-.69 4.89 1.4 6.22 1.46.93 3.32.93 4.79.02L22 23.39 23.39 22l-3.08-3.1m-3.81.1a2.5 2.5 0 0 1-2.5-2.5 2.5 2.5 0 0 1 2.5-2.5 2.5 2.5 0 0 1 2.5 2.5 2.5 2.5 0 0 1-2.5 2.5z"/></svg>');
|
||||
--md-source-forks-icon: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M5 3.25a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0zm0 2.122a2.25 2.25 0 1 0-1.5 0v.878A2.25 2.25 0 0 0 5.75 8.5h1.5v2.128a2.251 2.251 0 1 0 1.5 0V8.5h1.5a2.25 2.25 0 0 0 2.25-2.25v-.878a2.25 2.25 0 1 0-1.5 0v.878a.75.75 0 0 1-.75.75h-4.5A.75.75 0 0 1 5 6.25v-.878zm3.75 7.378a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0zm3-8.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5z"/></svg>');
|
||||
--md-source-repositories-icon: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M2 2.5A2.5 2.5 0 0 1 4.5 0h8.75a.75.75 0 0 1 .75.75v12.5a.75.75 0 0 1-.75.75h-2.5a.75.75 0 1 1 0-1.5h1.75v-2h-8a1 1 0 0 0-.714 1.7.75.75 0 0 1-1.072 1.05A2.495 2.495 0 0 1 2 11.5v-9zm10.5-1V9h-8c-.356 0-.694.074-1 .208V2.5a1 1 0 0 1 1-1h8zM5 12.25v3.25a.25.25 0 0 0 .4.2l1.45-1.087a.25.25 0 0 1 .3 0L8.6 15.7a.25.25 0 0 0 .4-.2v-3.25a.25.25 0 0 0-.25-.25h-3.5a.25.25 0 0 0-.25.25z"/></svg>');
|
||||
--md-source-stars-icon: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.75.75 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25zm0 2.445L6.615 5.5a.75.75 0 0 1-.564.41l-3.097.45 2.24 2.184a.75.75 0 0 1 .216.664l-.528 3.084 2.769-1.456a.75.75 0 0 1 .698 0l2.77 1.456-.53-3.084a.75.75 0 0 1 .216-.664l2.24-2.183-3.096-.45a.75.75 0 0 1-.564-.41L8 2.694v.001z"/></svg>');
|
||||
--md-source-version-icon: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M2.5 7.775V2.75a.25.25 0 0 1 .25-.25h5.025a.25.25 0 0 1 .177.073l6.25 6.25a.25.25 0 0 1 0 .354l-5.025 5.025a.25.25 0 0 1-.354 0l-6.25-6.25a.25.25 0 0 1-.073-.177zm-1.5 0V2.75C1 1.784 1.784 1 2.75 1h5.025c.464 0 .91.184 1.238.513l6.25 6.25a1.75 1.75 0 0 1 0 2.474l-5.026 5.026a1.75 1.75 0 0 1-2.474 0l-6.25-6.25A1.75 1.75 0 0 1 1 7.775zM6 5a1 1 0 1 0 0 2 1 1 0 0 0 0-2z"/></svg>');
|
||||
--md-version-icon: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><!--! Font Awesome Free 6.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2022 Fonticons, Inc.--><path d="m310.6 246.6-127.1 128c-7.1 6.3-15.3 9.4-23.5 9.4s-16.38-3.125-22.63-9.375l-127.1-128C.224 237.5-2.516 223.7 2.438 211.8S19.07 192 32 192h255.1c12.94 0 24.62 7.781 29.58 19.75s3.12 25.75-6.08 34.85z"/></svg>');
|
||||
}
|
||||
|
||||
:host > * {
|
||||
/* CODE */
|
||||
--md-code-fg-color: ${theme.palette.text.primary};
|
||||
--md-code-bg-color: ${theme.palette.background.paper};
|
||||
--md-code-hl-color: ${alpha(theme.palette.warning.main, 0.5)};
|
||||
--md-code-hl-keyword-color: ${
|
||||
isDarkTheme
|
||||
? theme.palette.primary.light
|
||||
: theme.palette.primary.dark
|
||||
};
|
||||
--md-code-hl-function-color: ${
|
||||
isDarkTheme
|
||||
? theme.palette.secondary.light
|
||||
: theme.palette.secondary.dark
|
||||
};
|
||||
--md-code-hl-string-color: ${
|
||||
isDarkTheme
|
||||
? theme.palette.success.light
|
||||
: theme.palette.success.dark
|
||||
};
|
||||
--md-code-hl-number-color: ${
|
||||
isDarkTheme
|
||||
? theme.palette.error.light
|
||||
: theme.palette.error.dark
|
||||
};
|
||||
--md-code-hl-constant-color: var(--md-code-hl-function-color);
|
||||
--md-code-hl-special-color: var(--md-code-hl-function-color);
|
||||
--md-code-hl-name-color: var(--md-code-fg-color);
|
||||
--md-code-hl-comment-color: var(--md-default-fg-color--light);
|
||||
--md-code-hl-generic-color: var(--md-default-fg-color--light);
|
||||
--md-code-hl-variable-color: var(--md-default-fg-color--light);
|
||||
--md-code-hl-operator-color: var(--md-default-fg-color--light);
|
||||
--md-code-hl-punctuation-color: var(--md-default-fg-color--light);
|
||||
|
||||
/* TYPESET */
|
||||
--md-typeset-font-size: 1rem;
|
||||
--md-typeset-color: var(--md-default-fg-color);
|
||||
--md-typeset-a-color: var(--md-accent-fg-color);
|
||||
--md-typeset-table-color: ${theme.palette.text.primary};
|
||||
--md-typeset-del-color: ${
|
||||
isDarkTheme
|
||||
? alpha(theme.palette.error.dark, 0.5)
|
||||
: alpha(theme.palette.error.light, 0.5)
|
||||
};
|
||||
--md-typeset-ins-color: ${
|
||||
isDarkTheme
|
||||
? alpha(theme.palette.success.dark, 0.5)
|
||||
: alpha(theme.palette.success.light, 0.5)
|
||||
};
|
||||
--md-typeset-mark-color: ${
|
||||
isDarkTheme
|
||||
? alpha(theme.palette.warning.dark, 0.5)
|
||||
: alpha(theme.palette.warning.light, 0.5)
|
||||
};
|
||||
}
|
||||
|
||||
@media screen and (max-width: 76.1875em) {
|
||||
:host > * {
|
||||
/* TYPESET */
|
||||
--md-typeset-font-size: .9rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
:host > * {
|
||||
/* TYPESET */
|
||||
--md-typeset-font-size: .7rem;
|
||||
}
|
||||
}
|
||||
`,
|
||||
}),
|
||||
injectCss({
|
||||
// Reset
|
||||
css: `
|
||||
body {
|
||||
--md-text-color: var(--md-default-fg-color);
|
||||
--md-text-link-color: var(--md-accent-fg-color);
|
||||
--md-text-font-family: ${theme.typography.fontFamily};
|
||||
font-family: var(--md-text-font-family);
|
||||
background-color: unset;
|
||||
}
|
||||
`,
|
||||
}),
|
||||
injectCss({
|
||||
// Layout
|
||||
css: `
|
||||
.md-grid {
|
||||
max-width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.md-nav {
|
||||
font-size: calc(var(--md-typeset-font-size) * 0.9);
|
||||
}
|
||||
.md-nav__link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.md-nav__icon {
|
||||
height: 20px !important;
|
||||
width: 20px !important;
|
||||
margin-left:${theme.spacing(1)}px;
|
||||
}
|
||||
.md-nav__icon svg {
|
||||
margin: 0;
|
||||
width: 20px !important;
|
||||
height: 20px !important;
|
||||
}
|
||||
.md-nav__icon:after {
|
||||
width: 20px !important;
|
||||
height: 20px !important;
|
||||
}
|
||||
|
||||
.md-main__inner {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.md-sidebar {
|
||||
bottom: 75px;
|
||||
position: fixed;
|
||||
width: 16rem;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
scrollbar-color: rgb(193, 193, 193) #eee;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
.md-sidebar .md-sidebar__scrollwrap {
|
||||
width: calc(16rem - 10px);
|
||||
}
|
||||
.md-sidebar--secondary {
|
||||
right: ${theme.spacing(3)}px;
|
||||
}
|
||||
.md-sidebar::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
}
|
||||
.md-sidebar::-webkit-scrollbar-button {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
}
|
||||
.md-sidebar::-webkit-scrollbar-track {
|
||||
background: #eee;
|
||||
border: 1 px solid rgb(250, 250, 250);
|
||||
box-shadow: 0px 0px 3px #dfdfdf inset;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.md-sidebar::-webkit-scrollbar-thumb {
|
||||
width: 5px;
|
||||
background: rgb(193, 193, 193);
|
||||
border: transparent;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.md-sidebar::-webkit-scrollbar-thumb:hover {
|
||||
background: rgb(125, 125, 125);
|
||||
}
|
||||
|
||||
.md-content {
|
||||
max-width: calc(100% - 16rem * 2);
|
||||
margin-left: 16rem;
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
|
||||
.md-footer {
|
||||
position: fixed;
|
||||
bottom: 0px;
|
||||
}
|
||||
.md-footer__title {
|
||||
background-color: unset;
|
||||
}
|
||||
.md-footer-nav__link {
|
||||
width: 16rem;
|
||||
}
|
||||
|
||||
.md-dialog {
|
||||
background-color: unset;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 76.25em) {
|
||||
.md-sidebar {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 76.1875em) {
|
||||
.md-nav {
|
||||
transition: none !important;
|
||||
background-color: var(--md-default-bg-color)
|
||||
}
|
||||
.md-nav--primary .md-nav__title {
|
||||
cursor: auto;
|
||||
color: var(--md-default-fg-color);
|
||||
font-weight: 700;
|
||||
white-space: normal;
|
||||
line-height: 1rem;
|
||||
height: auto;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
row-gap: 1.6rem;
|
||||
padding: 1.2rem .8rem .8rem;
|
||||
background-color: var(--md-default-bg-color);
|
||||
}
|
||||
.md-nav--primary .md-nav__title~.md-nav__list {
|
||||
box-shadow: none;
|
||||
}
|
||||
.md-nav--primary .md-nav__title ~ .md-nav__list > :first-child {
|
||||
border-top: none;
|
||||
}
|
||||
.md-nav--primary .md-nav__title .md-nav__button {
|
||||
display: none;
|
||||
}
|
||||
.md-nav--primary .md-nav__title .md-nav__icon {
|
||||
color: var(--md-default-fg-color);
|
||||
position: static;
|
||||
height: auto;
|
||||
margin: 0 0 0 -0.2rem;
|
||||
}
|
||||
.md-nav--primary > .md-nav__title [for="none"] {
|
||||
padding-top: 0;
|
||||
}
|
||||
.md-nav--primary .md-nav__item {
|
||||
border-top: none;
|
||||
}
|
||||
.md-nav--primary :is(.md-nav__title,.md-nav__item) {
|
||||
font-size : var(--md-typeset-font-size);
|
||||
}
|
||||
.md-nav .md-source {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.md-sidebar {
|
||||
height: 100%;
|
||||
}
|
||||
.md-sidebar--primary {
|
||||
width: 12.1rem !important;
|
||||
z-index: 200;
|
||||
left: ${
|
||||
isPinned
|
||||
? 'calc(-12.1rem + 242px)'
|
||||
: 'calc(-12.1rem + 72px)'
|
||||
} !important;
|
||||
}
|
||||
.md-sidebar--secondary:not([hidden]) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.md-content {
|
||||
max-width: 100%;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.md-header__button {
|
||||
margin: 0.4rem 0;
|
||||
margin-left: 0.4rem;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.md-overlay {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.md-footer {
|
||||
position: static;
|
||||
padding-left: 0;
|
||||
}
|
||||
.md-footer-nav__link {
|
||||
/* footer links begin to overlap at small sizes without setting width */
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
.md-sidebar--primary {
|
||||
left: -12.1rem !important;
|
||||
width: 12.1rem;
|
||||
}
|
||||
}
|
||||
`,
|
||||
}),
|
||||
injectCss({
|
||||
// Typeset
|
||||
css: `
|
||||
.md-typeset {
|
||||
font-size: var(--md-typeset-font-size);
|
||||
}
|
||||
|
||||
${headings.reduce<string>((style, heading) => {
|
||||
const styles = theme.typography[heading];
|
||||
const { lineHeight, fontFamily, fontWeight, fontSize } = styles;
|
||||
const calculate = (value: typeof fontSize) => {
|
||||
let factor: number | string = 1;
|
||||
if (typeof value === 'number') {
|
||||
// 60% of the size defined because it is too big
|
||||
factor = (value / 16) * 0.6;
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
factor = value.replace('rem', '');
|
||||
}
|
||||
return `calc(${factor} * var(--md-typeset-font-size))`;
|
||||
};
|
||||
return style.concat(`
|
||||
.md-typeset ${heading} {
|
||||
color: var(--md-default-fg-color);
|
||||
line-height: ${lineHeight};
|
||||
font-family: ${fontFamily};
|
||||
font-weight: ${fontWeight};
|
||||
font-size: ${calculate(fontSize)};
|
||||
}
|
||||
`);
|
||||
}, '')}
|
||||
|
||||
.md-typeset .md-content__button {
|
||||
color: var(--md-default-fg-color);
|
||||
}
|
||||
|
||||
.md-typeset hr {
|
||||
border-bottom: 0.05rem dotted ${theme.palette.divider};
|
||||
}
|
||||
|
||||
.md-typeset details {
|
||||
font-size: var(--md-typeset-font-size) !important;
|
||||
}
|
||||
.md-typeset details summary {
|
||||
padding-left: 2.5rem !important;
|
||||
}
|
||||
.md-typeset details summary:before,
|
||||
.md-typeset details summary:after {
|
||||
top: 50% !important;
|
||||
width: 20px !important;
|
||||
height: 20px !important;
|
||||
transform: rotate(0deg) translateY(-50%) !important;
|
||||
}
|
||||
.md-typeset details[open] > summary:after {
|
||||
transform: rotate(90deg) translateX(-50%) !important;
|
||||
}
|
||||
|
||||
.md-typeset blockquote {
|
||||
color: var(--md-default-fg-color--light);
|
||||
border-left: 0.2rem solid var(--md-default-fg-color--light);
|
||||
}
|
||||
|
||||
.md-typeset table:not([class]) {
|
||||
font-size: var(--md-typeset-font-size);
|
||||
border: 1px solid var(--md-default-fg-color);
|
||||
border-bottom: none;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.md-typeset table:not([class]) th {
|
||||
font-weight: bold;
|
||||
}
|
||||
.md-typeset table:not([class]) td, .md-typeset table:not([class]) th {
|
||||
border-bottom: 1px solid var(--md-default-fg-color);
|
||||
}
|
||||
|
||||
.md-typeset pre > code::-webkit-scrollbar-thumb {
|
||||
background-color: hsla(0, 0%, 0%, 0.32);
|
||||
}
|
||||
.md-typeset pre > code::-webkit-scrollbar-thumb:hover {
|
||||
background-color: hsla(0, 0%, 0%, 0.87);
|
||||
}
|
||||
`,
|
||||
}),
|
||||
injectCss({
|
||||
// Animations
|
||||
css: `
|
||||
/*
|
||||
Disable CSS animations on link colors as they lead to issues in dark mode.
|
||||
The dark mode color theme is applied later and theirfore there is always an animation from light to dark mode when navigation between pages.
|
||||
*/
|
||||
.md-dialog, .md-nav__link, .md-footer__link, .md-typeset a, .md-typeset a::before, .md-typeset .headerlink {
|
||||
transition: none;
|
||||
}
|
||||
`,
|
||||
}),
|
||||
injectCss({
|
||||
// Extensions
|
||||
css: `
|
||||
/* HIGHLIGHT */
|
||||
.highlight .md-clipboard:after {
|
||||
content: unset;
|
||||
}
|
||||
|
||||
.highlight .nx {
|
||||
color: ${isDarkTheme ? '#ff53a3' : '#ec407a'};
|
||||
}
|
||||
|
||||
/* CODE HILITE */
|
||||
.codehilite .gd {
|
||||
background-color: ${
|
||||
isDarkTheme ? 'rgba(248,81,73,0.65)' : '#fdd'
|
||||
};
|
||||
}
|
||||
|
||||
.codehilite .gi {
|
||||
background-color: ${
|
||||
isDarkTheme ? 'rgba(46,160,67,0.65)' : '#dfd'
|
||||
};
|
||||
}
|
||||
|
||||
/* TABBED */
|
||||
.tabbed-set>input:nth-child(1):checked~.tabbed-labels>:nth-child(1),
|
||||
.tabbed-set>input:nth-child(2):checked~.tabbed-labels>:nth-child(2),
|
||||
.tabbed-set>input:nth-child(3):checked~.tabbed-labels>:nth-child(3),
|
||||
.tabbed-set>input:nth-child(4):checked~.tabbed-labels>:nth-child(4),
|
||||
.tabbed-set>input:nth-child(5):checked~.tabbed-labels>:nth-child(5),
|
||||
.tabbed-set>input:nth-child(6):checked~.tabbed-labels>:nth-child(6),
|
||||
.tabbed-set>input:nth-child(7):checked~.tabbed-labels>:nth-child(7),
|
||||
.tabbed-set>input:nth-child(8):checked~.tabbed-labels>:nth-child(8),
|
||||
.tabbed-set>input:nth-child(9):checked~.tabbed-labels>:nth-child(9),
|
||||
.tabbed-set>input:nth-child(10):checked~.tabbed-labels>:nth-child(10),
|
||||
.tabbed-set>input:nth-child(11):checked~.tabbed-labels>:nth-child(11),
|
||||
.tabbed-set>input:nth-child(12):checked~.tabbed-labels>:nth-child(12),
|
||||
.tabbed-set>input:nth-child(13):checked~.tabbed-labels>:nth-child(13),
|
||||
.tabbed-set>input:nth-child(14):checked~.tabbed-labels>:nth-child(14),
|
||||
.tabbed-set>input:nth-child(15):checked~.tabbed-labels>:nth-child(15),
|
||||
.tabbed-set>input:nth-child(16):checked~.tabbed-labels>:nth-child(16),
|
||||
.tabbed-set>input:nth-child(17):checked~.tabbed-labels>:nth-child(17),
|
||||
.tabbed-set>input:nth-child(18):checked~.tabbed-labels>:nth-child(18),
|
||||
.tabbed-set>input:nth-child(19):checked~.tabbed-labels>:nth-child(19),
|
||||
.tabbed-set>input:nth-child(20):checked~.tabbed-labels>:nth-child(20) {
|
||||
color: var(--md-accent-fg-color);
|
||||
border-color: var(--md-accent-fg-color);
|
||||
}
|
||||
|
||||
/* TASK-LIST */
|
||||
.task-list-control .task-list-indicator::before {
|
||||
background-color: ${theme.palette.action.disabledBackground};
|
||||
}
|
||||
.task-list-control [type="checkbox"]:checked + .task-list-indicator:before {
|
||||
background-color: ${theme.palette.success.main};
|
||||
}
|
||||
|
||||
/* ADMONITION */
|
||||
.admonition {
|
||||
font-size: var(--md-typeset-font-size) !important;
|
||||
}
|
||||
.admonition .admonition-title {
|
||||
padding-left: 2.5rem !important;
|
||||
}
|
||||
|
||||
.admonition .admonition-title:before {
|
||||
top: 50% !important;
|
||||
width: 20px !important;
|
||||
height: 20px !important;
|
||||
transform: translateY(-50%) !important;
|
||||
}
|
||||
`,
|
||||
}),
|
||||
]),
|
||||
[
|
||||
kind,
|
||||
name,
|
||||
namespace,
|
||||
scmIntegrationsApi,
|
||||
techdocsSanitizer,
|
||||
techdocsStorageApi,
|
||||
theme,
|
||||
isDarkTheme,
|
||||
isPinned,
|
||||
],
|
||||
);
|
||||
|
||||
// a function that performs transformations that are executed after adding it to the DOM
|
||||
const postRender = useCallback(
|
||||
async (transformedElement: Element) =>
|
||||
transformer(transformedElement, [
|
||||
scrollIntoAnchor(),
|
||||
copyToClipboard(theme),
|
||||
addLinkClickListener({
|
||||
baseUrl: window.location.origin,
|
||||
onClick: (event: MouseEvent, url: string) => {
|
||||
// detect if CTRL or META keys are pressed so that links can be opened in a new tab with `window.open`
|
||||
const modifierActive = event.ctrlKey || event.metaKey;
|
||||
const parsedUrl = new URL(url);
|
||||
|
||||
// hash exists when anchor is clicked on secondary sidebar
|
||||
if (parsedUrl.hash) {
|
||||
if (modifierActive) {
|
||||
window.open(`${parsedUrl.pathname}${parsedUrl.hash}`, '_blank');
|
||||
} else {
|
||||
navigate(`${parsedUrl.pathname}${parsedUrl.hash}`);
|
||||
// Scroll to hash if it's on the current page
|
||||
transformedElement
|
||||
?.querySelector(`#${parsedUrl.hash.slice(1)}`)
|
||||
?.scrollIntoView();
|
||||
}
|
||||
} else {
|
||||
if (modifierActive) {
|
||||
window.open(parsedUrl.pathname, '_blank');
|
||||
} else {
|
||||
navigate(parsedUrl.pathname);
|
||||
}
|
||||
}
|
||||
},
|
||||
}),
|
||||
onCssReady({
|
||||
docStorageUrl: await techdocsStorageApi.getApiOrigin(),
|
||||
onLoading: (renderedElement: Element) => {
|
||||
(renderedElement as HTMLElement).style.setProperty('opacity', '0');
|
||||
},
|
||||
onLoaded: (renderedElement: Element) => {
|
||||
(renderedElement as HTMLElement).style.removeProperty('opacity');
|
||||
// disable MkDocs drawer toggling ('for' attribute => checkbox mechanism)
|
||||
renderedElement
|
||||
.querySelector('.md-nav__title')
|
||||
?.removeAttribute('for');
|
||||
setSidebars(
|
||||
Array.from(renderedElement.querySelectorAll('.md-sidebar')),
|
||||
);
|
||||
},
|
||||
}),
|
||||
]),
|
||||
[theme, navigate, techdocsStorageApi],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!rawPage) return () => {};
|
||||
|
||||
// if false, there is already a newer execution of this effect
|
||||
let shouldReplaceContent = true;
|
||||
|
||||
// Pre-render
|
||||
preRender(rawPage, path).then(async preTransformedDomElement => {
|
||||
if (!preTransformedDomElement?.innerHTML) {
|
||||
return; // An unexpected error occurred
|
||||
}
|
||||
|
||||
// don't manipulate the shadow dom if this isn't the latest effect execution
|
||||
if (!shouldReplaceContent) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Scroll to top after render
|
||||
window.scroll({ top: 0 });
|
||||
|
||||
// Post-render
|
||||
const postTransformedDomElement = await postRender(
|
||||
preTransformedDomElement,
|
||||
);
|
||||
setDom(postTransformedDomElement as HTMLElement);
|
||||
});
|
||||
|
||||
// cancel this execution
|
||||
return () => {
|
||||
shouldReplaceContent = false;
|
||||
};
|
||||
}, [rawPage, path, preRender, postRender]);
|
||||
|
||||
return dom;
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* 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, Reader } from './TechDocsReaderPageContent';
|
||||
export type { TechDocsReaderPageContentProps } from './TechDocsReaderPageContent';
|
||||
export * from './context';
|
||||
export * from './dom';
|
||||
@@ -1,115 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 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 { TechDocsReaderPageHeader } from './TechDocsReaderPageHeader';
|
||||
import { act } from '@testing-library/react';
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
import { entityRouteRef } from '@backstage/plugin-catalog-react';
|
||||
import { rootRouteRef } from '../../routes';
|
||||
|
||||
describe('<TechDocsReaderPageHeader />', () => {
|
||||
it('should render a techdocs page header', async () => {
|
||||
await act(async () => {
|
||||
const rendered = await renderInTestApp(
|
||||
<TechDocsReaderPageHeader
|
||||
entityRef={{
|
||||
kind: 'test',
|
||||
name: 'test-name',
|
||||
namespace: 'test-namespace',
|
||||
}}
|
||||
entityMetadata={{
|
||||
locationMetadata: {
|
||||
type: 'github',
|
||||
target: 'https://example.com/',
|
||||
},
|
||||
apiVersion: 'v1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'test',
|
||||
},
|
||||
spec: {
|
||||
owner: 'test',
|
||||
},
|
||||
}}
|
||||
techDocsMetadata={{
|
||||
site_name: 'test-site-name',
|
||||
site_description: 'test-site-desc',
|
||||
}}
|
||||
/>,
|
||||
{
|
||||
mountedRoutes: {
|
||||
'/catalog/:namespace/:kind/:name/*': entityRouteRef,
|
||||
'/docs': rootRouteRef,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(rendered.container.innerHTML).toContain('header');
|
||||
expect(rendered.getAllByText('test-site-name')).toHaveLength(2);
|
||||
expect(rendered.getByText('test-site-desc')).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it('should render a techdocs page header even if metadata is missing', async () => {
|
||||
await act(async () => {
|
||||
const rendered = await renderInTestApp(
|
||||
<TechDocsReaderPageHeader
|
||||
entityRef={{
|
||||
kind: 'test',
|
||||
name: 'test-name',
|
||||
namespace: 'test-namespace',
|
||||
}}
|
||||
/>,
|
||||
{
|
||||
mountedRoutes: {
|
||||
'/catalog/:namespace/:kind/:name/*': entityRouteRef,
|
||||
'/docs': rootRouteRef,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(rendered.container.innerHTML).toContain('header');
|
||||
});
|
||||
});
|
||||
|
||||
it('should render a link back to the component page', async () => {
|
||||
await act(async () => {
|
||||
const rendered = await renderInTestApp(
|
||||
<TechDocsReaderPageHeader
|
||||
entityRef={{
|
||||
kind: 'test',
|
||||
name: 'test-name',
|
||||
namespace: 'test-namespace',
|
||||
}}
|
||||
techDocsMetadata={{
|
||||
site_name: 'test-site-name',
|
||||
site_description: 'test-site-desc',
|
||||
}}
|
||||
/>,
|
||||
{
|
||||
mountedRoutes: {
|
||||
'/catalog/:namespace/:kind/:name/*': entityRouteRef,
|
||||
'/docs': rootRouteRef,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(rendered.container.innerHTML).toContain(
|
||||
'/catalog/test-namespace/test/test-name',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* Copyright 2020 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 { act, waitFor } from '@testing-library/react';
|
||||
|
||||
import { ThemeProvider } from '@material-ui/core';
|
||||
|
||||
import { lightTheme } from '@backstage/theme';
|
||||
import { CompoundEntityRef } from '@backstage/catalog-model';
|
||||
import { entityRouteRef } from '@backstage/plugin-catalog-react';
|
||||
import {
|
||||
techdocsApiRef,
|
||||
TechDocsReaderPageProvider,
|
||||
} from '@backstage/plugin-techdocs-react';
|
||||
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
|
||||
|
||||
import { rootRouteRef } from '../../../routes';
|
||||
|
||||
import { TechDocsReaderPageHeader } from './TechDocsReaderPageHeader';
|
||||
|
||||
const mockEntityMetadata = {
|
||||
locationMetadata: {
|
||||
type: 'github',
|
||||
target: 'https://example.com/',
|
||||
},
|
||||
apiVersion: 'v1',
|
||||
kind: 'test',
|
||||
metadata: {
|
||||
name: 'test-name',
|
||||
namespace: 'test-namespace',
|
||||
},
|
||||
spec: {
|
||||
owner: 'test',
|
||||
},
|
||||
};
|
||||
|
||||
const mockTechDocsMetadata = {
|
||||
site_name: 'test-site-name',
|
||||
site_description: 'test-site-desc',
|
||||
};
|
||||
|
||||
const getEntityMetadata = jest.fn();
|
||||
const getTechDocsMetadata = jest.fn();
|
||||
|
||||
const techdocsApiMock = {
|
||||
getEntityMetadata,
|
||||
getTechDocsMetadata,
|
||||
};
|
||||
|
||||
const Wrapper = ({
|
||||
entityRef = {
|
||||
kind: mockEntityMetadata.kind,
|
||||
name: mockEntityMetadata.metadata.name,
|
||||
namespace: mockEntityMetadata.metadata.namespace!!,
|
||||
},
|
||||
children,
|
||||
}: {
|
||||
entityRef?: CompoundEntityRef;
|
||||
children: React.ReactNode;
|
||||
}) => (
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<TestApiProvider apis={[[techdocsApiRef, techdocsApiMock]]}>
|
||||
<TechDocsReaderPageProvider entityRef={entityRef}>
|
||||
{children}
|
||||
</TechDocsReaderPageProvider>
|
||||
</TestApiProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
describe('<TechDocsReaderPageHeader />', () => {
|
||||
it('should render a techdocs page header', async () => {
|
||||
getEntityMetadata.mockResolvedValue(mockEntityMetadata);
|
||||
getTechDocsMetadata.mockResolvedValue(mockTechDocsMetadata);
|
||||
|
||||
await act(async () => {
|
||||
const rendered = await renderInTestApp(
|
||||
<Wrapper>
|
||||
<TechDocsReaderPageHeader />
|
||||
</Wrapper>,
|
||||
{
|
||||
mountedRoutes: {
|
||||
'/catalog/:namespace/:kind/:name/*': entityRouteRef,
|
||||
'/docs': rootRouteRef,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(rendered.container.innerHTML).toContain('header');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(rendered.getAllByText('test-site-name')).toHaveLength(2);
|
||||
});
|
||||
|
||||
expect(rendered.getByText('test-site-desc')).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it('should render a techdocs page header even if metadata is missing', async () => {
|
||||
await act(async () => {
|
||||
const rendered = await renderInTestApp(
|
||||
<Wrapper>
|
||||
<TechDocsReaderPageHeader />
|
||||
</Wrapper>,
|
||||
{
|
||||
mountedRoutes: {
|
||||
'/catalog/:namespace/:kind/:name/*': entityRouteRef,
|
||||
'/docs': rootRouteRef,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(rendered.container.innerHTML).toContain('header');
|
||||
});
|
||||
});
|
||||
|
||||
it('should render a link back to the component page', async () => {
|
||||
getTechDocsMetadata.mockResolvedValue(mockTechDocsMetadata);
|
||||
|
||||
await act(async () => {
|
||||
const rendered = await renderInTestApp(
|
||||
<Wrapper>
|
||||
<TechDocsReaderPageHeader />
|
||||
</Wrapper>,
|
||||
{
|
||||
mountedRoutes: {
|
||||
'/catalog/:namespace/:kind/:name/*': entityRouteRef,
|
||||
'/docs': rootRouteRef,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
rendered.getByRole('link', { name: 'test:test-namespace/test-name' }),
|
||||
).toHaveAttribute('href', '/catalog/test-namespace/test/test-name');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
+60
-20
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
* 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.
|
||||
@@ -14,45 +14,84 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { PropsWithChildren } from 'react';
|
||||
import React, { PropsWithChildren, useEffect } from 'react';
|
||||
import Helmet from 'react-helmet';
|
||||
|
||||
import { Skeleton } from '@material-ui/lab';
|
||||
import CodeIcon from '@material-ui/icons/Code';
|
||||
|
||||
import { useRouteRef } from '@backstage/core-plugin-api';
|
||||
import { Header, HeaderLabel } from '@backstage/core-components';
|
||||
import { CompoundEntityRef, RELATION_OWNED_BY } from '@backstage/catalog-model';
|
||||
import {
|
||||
TechDocsAddonLocations as locations,
|
||||
useTechDocsAddons,
|
||||
useTechDocsReaderPage,
|
||||
TechDocsEntityMetadata,
|
||||
TechDocsMetadata,
|
||||
} from '@backstage/plugin-techdocs-react';
|
||||
import {
|
||||
EntityRefLink,
|
||||
EntityRefLinks,
|
||||
getEntityRelations,
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
import { RELATION_OWNED_BY, CompoundEntityRef } from '@backstage/catalog-model';
|
||||
import { Header, HeaderLabel } from '@backstage/core-components';
|
||||
import { useRouteRef, configApiRef, useApi } from '@backstage/core-plugin-api';
|
||||
|
||||
import { rootRouteRef } from '../../routes';
|
||||
import { TechDocsEntityMetadata, TechDocsMetadata } from '../../types';
|
||||
import { rootRouteRef } from '../../../routes';
|
||||
|
||||
const skeleton = <Skeleton animation="wave" variant="text" height={40} />;
|
||||
|
||||
/**
|
||||
* Props for {@link TechDocsReaderPageHeader}
|
||||
*
|
||||
* @public
|
||||
* @deprecated No need to pass down properties anymore. The component consumes data from `TechDocsReaderPageContext` instead. Use the {@link @backstage/plugin-techdocs-react#useTechDocsReaderPage} hook for custom header.
|
||||
*/
|
||||
export type TechDocsReaderPageHeaderProps = PropsWithChildren<{
|
||||
entityRef: CompoundEntityRef;
|
||||
entityRef?: CompoundEntityRef;
|
||||
entityMetadata?: TechDocsEntityMetadata;
|
||||
techDocsMetadata?: TechDocsMetadata;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* Component responsible for rendering a Header with metadata on TechDocs reader page.
|
||||
*
|
||||
* Renders the reader page header.
|
||||
* This component does not accept props, please use
|
||||
* the Tech Docs add-ons to customize it
|
||||
* @public
|
||||
*/
|
||||
export const TechDocsReaderPageHeader = (
|
||||
props: TechDocsReaderPageHeaderProps,
|
||||
) => {
|
||||
const { entityRef, entityMetadata, techDocsMetadata, children } = props;
|
||||
const { name } = entityRef;
|
||||
const { children } = props;
|
||||
const addons = useTechDocsAddons();
|
||||
const configApi = useApi(configApiRef);
|
||||
|
||||
const { site_name: siteName, site_description: siteDescription } =
|
||||
techDocsMetadata || {};
|
||||
const {
|
||||
title,
|
||||
setTitle,
|
||||
subtitle,
|
||||
setSubtitle,
|
||||
entityRef,
|
||||
metadata: { value: metadata },
|
||||
entityMetadata: { value: entityMetadata },
|
||||
} = useTechDocsReaderPage();
|
||||
|
||||
useEffect(() => {
|
||||
if (!metadata) return;
|
||||
setTitle(prevTitle => {
|
||||
const { site_name } = metadata;
|
||||
return prevTitle || site_name;
|
||||
});
|
||||
setSubtitle(prevSubtitle => {
|
||||
let { site_description } = metadata;
|
||||
if (!site_description || site_description === 'None') {
|
||||
site_description = 'Home';
|
||||
}
|
||||
return prevSubtitle || site_description;
|
||||
});
|
||||
}, [metadata, setTitle, setSubtitle]);
|
||||
|
||||
const appTitle = configApi.getOptional('app.title') || 'Backstage';
|
||||
const tabTitle = [subtitle, title, appTitle].filter(Boolean).join(' | ');
|
||||
|
||||
const { locationMetadata, spec } = entityMetadata || {};
|
||||
const lifecycle = spec?.lifecycle;
|
||||
@@ -109,16 +148,17 @@ export const TechDocsReaderPageHeader = (
|
||||
|
||||
return (
|
||||
<Header
|
||||
title={siteName ? siteName : '.'}
|
||||
pageTitleOverride={siteName || name}
|
||||
subtitle={
|
||||
siteDescription && siteDescription !== 'None' ? siteDescription : ''
|
||||
}
|
||||
type="Docs"
|
||||
type="Documentation"
|
||||
typeLink={docsRootLink}
|
||||
title={title || skeleton}
|
||||
subtitle={subtitle || skeleton}
|
||||
>
|
||||
<Helmet titleTemplate="%s">
|
||||
<title>{tabTitle}</title>
|
||||
</Helmet>
|
||||
{labels}
|
||||
{children}
|
||||
{addons.renderComponentsByLocation(locations.Header)}
|
||||
</Header>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* 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';
|
||||
export type { TechDocsReaderPageHeaderProps } from './TechDocsReaderPageHeader';
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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 { Box, Toolbar, ToolbarProps, withStyles } from '@material-ui/core';
|
||||
|
||||
import {
|
||||
TechDocsAddonLocations as locations,
|
||||
useTechDocsAddons,
|
||||
} from '@backstage/plugin-techdocs-react';
|
||||
|
||||
/**
|
||||
* Renders the reader page subheader.
|
||||
* Please use the Tech Docs add-ons to customize it
|
||||
* @public
|
||||
*/
|
||||
export const TechDocsReaderPageSubheader = withStyles(theme => ({
|
||||
root: {
|
||||
gridArea: 'pageSubheader',
|
||||
flexDirection: 'column',
|
||||
minHeight: 'auto',
|
||||
padding: theme.spacing(3, 3, 0),
|
||||
},
|
||||
}))(({ toolbarProps }: { toolbarProps?: ToolbarProps }) => {
|
||||
const addons = useTechDocsAddons();
|
||||
const subheaderAddons = addons.renderComponentsByLocation(
|
||||
locations.Subheader,
|
||||
);
|
||||
|
||||
if (!subheaderAddons) return null;
|
||||
|
||||
return (
|
||||
<Toolbar {...toolbarProps}>
|
||||
{subheaderAddons && (
|
||||
<Box
|
||||
display="flex"
|
||||
justifyContent="flex-end"
|
||||
width="100%"
|
||||
flexWrap="wrap"
|
||||
>
|
||||
{subheaderAddons}
|
||||
</Box>
|
||||
)}
|
||||
</Toolbar>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* 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';
|
||||
@@ -21,7 +21,7 @@ import { Alert } from '@material-ui/lab';
|
||||
|
||||
import { TechDocsBuildLogs } from './TechDocsBuildLogs';
|
||||
import { TechDocsNotFound } from './TechDocsNotFound';
|
||||
import { useTechDocsReader } from './Reader';
|
||||
import { useTechDocsReader } from './TechDocsReaderPageContent';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
root: {
|
||||
|
||||
@@ -14,23 +14,13 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './Reader';
|
||||
export type {
|
||||
TechDocsReaderPageProps,
|
||||
TechDocsReaderPageRenderFunction,
|
||||
TechDocsReaderLayoutProps,
|
||||
} from './TechDocsReaderPage';
|
||||
export { TechDocsReaderLayout } from './TechDocsReaderPage';
|
||||
export * from './TechDocsReaderPageHeader';
|
||||
export * from './TechDocsReaderPageContent';
|
||||
export * from './TechDocsReaderPageSubheader';
|
||||
export * from './TechDocsStateIndicator';
|
||||
|
||||
/**
|
||||
* Note: this component is currently being exported so that we can rapidly
|
||||
* iterate on alternative <Reader /> implementations that extend core
|
||||
* functionality. There is no guarantee that this component will continue to be
|
||||
* exported by the package in the future!
|
||||
*
|
||||
* Why is this comment here instead of above the component itself? It's a
|
||||
* workaround for some kind of bug in @microsoft/api-extractor.
|
||||
*
|
||||
* todo: Make public or stop exporting (ctrl+f "altReaderExperiments")
|
||||
* @internal
|
||||
*/
|
||||
export type { ReaderState, ContentStateTypes } from './useReaderState';
|
||||
|
||||
@@ -21,9 +21,10 @@ import useAsyncRetry from 'react-use/lib/useAsyncRetry';
|
||||
import { techdocsStorageApiRef } from '../../api';
|
||||
|
||||
/**
|
||||
* @public
|
||||
* A state representation that is used to configure the UI of <Reader />
|
||||
*/
|
||||
type ContentStateTypes =
|
||||
export type ContentStateTypes =
|
||||
/** There is nothing to display but a loading indicator */
|
||||
| 'CHECKING'
|
||||
|
||||
@@ -224,13 +225,10 @@ export function reducer(
|
||||
|
||||
return newState;
|
||||
}
|
||||
|
||||
export function useReaderState(
|
||||
kind: string,
|
||||
namespace: string,
|
||||
name: string,
|
||||
path: string,
|
||||
): {
|
||||
/**
|
||||
* @public shared reader state
|
||||
*/
|
||||
export type ReaderState = {
|
||||
state: ContentStateTypes;
|
||||
path: string;
|
||||
contentReload: () => void;
|
||||
@@ -238,7 +236,14 @@ export function useReaderState(
|
||||
contentErrorMessage?: string;
|
||||
syncErrorMessage?: string;
|
||||
buildLog: string[];
|
||||
} {
|
||||
};
|
||||
|
||||
export function useReaderState(
|
||||
kind: string,
|
||||
namespace: string,
|
||||
name: string,
|
||||
path: string,
|
||||
): ReaderState {
|
||||
const [state, dispatch] = useReducer(reducer, {
|
||||
activeSyncState: 'CHECKING',
|
||||
path,
|
||||
|
||||
@@ -14,23 +14,27 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { CompoundEntityRef } from '@backstage/catalog-model';
|
||||
import {
|
||||
TechDocsEntityMetadata,
|
||||
TechDocsMetadata,
|
||||
} from '@backstage/plugin-techdocs-react';
|
||||
|
||||
/**
|
||||
* Metadata for TechDocs page
|
||||
* Helper function that gives the children of {@link TechDocsReaderPage} access to techdocs and entity metadata
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type TechDocsMetadata = {
|
||||
site_name: string;
|
||||
site_description: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Metadata for TechDocs Entity
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type TechDocsEntityMetadata = Entity & {
|
||||
locationMetadata?: { type: string; target: string };
|
||||
};
|
||||
export type TechDocsReaderPageRenderFunction = ({
|
||||
techdocsMetadataValue,
|
||||
entityMetadataValue,
|
||||
entityRef,
|
||||
}: {
|
||||
techdocsMetadataValue?: TechDocsMetadata | undefined;
|
||||
entityMetadataValue?: TechDocsEntityMetadata | undefined;
|
||||
entityRef: CompoundEntityRef;
|
||||
/**
|
||||
* @deprecated You can continue pass this property, but directly to the `TechDocsReaderPageContent` component.
|
||||
*/
|
||||
onReady?: () => void;
|
||||
}) => JSX.Element;
|
||||
|
||||
@@ -257,14 +257,15 @@ const NO_WARNING_PACKAGES = [
|
||||
'plugins/scaffolder-common',
|
||||
'plugins/search-backend-node',
|
||||
'plugins/search-common',
|
||||
'plugins/techdocs',
|
||||
'plugins/techdocs-backend',
|
||||
'plugins/techdocs-node',
|
||||
'plugins/techdocs-react',
|
||||
'plugins/tech-insights',
|
||||
'plugins/tech-insights-backend',
|
||||
'plugins/tech-insights-backend-module-jsonfc',
|
||||
'plugins/tech-insights-common',
|
||||
'plugins/tech-insights-node',
|
||||
'plugins/techdocs',
|
||||
'plugins/todo',
|
||||
'plugins/todo-backend',
|
||||
];
|
||||
|
||||
@@ -6002,6 +6002,11 @@
|
||||
resolved "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f"
|
||||
integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==
|
||||
|
||||
"@types/event-source-polyfill@^1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.npmjs.org/@types/event-source-polyfill/-/event-source-polyfill-1.0.0.tgz#f93f13433f750c8ea0e3cfa69c72e3c7393e0585"
|
||||
integrity sha512-b8O8/rg7NIW0iJ8i9MNDBZqPljHA+b7AjC3QFqH3dSyW6vgrl3oBgyIv5dw2fibh5enHHDkkPZG5PHza7U4NRw==
|
||||
|
||||
"@types/expect@^1.20.4":
|
||||
version "1.20.4"
|
||||
resolved "https://registry.npmjs.org/@types/expect/-/expect-1.20.4.tgz#8288e51737bf7e3ab5d7c77bfa695883745264e5"
|
||||
@@ -7837,7 +7842,7 @@ array-ify@^1.0.0:
|
||||
resolved "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz#9e528762b4a9066ad163a6962a364418e9626ece"
|
||||
integrity sha1-nlKHYrSpBmrRY6aWKjZEGOlibs4=
|
||||
|
||||
array-includes@^3.1.3, array-includes@^3.1.4:
|
||||
array-includes@^3.1.2, array-includes@^3.1.3, array-includes@^3.1.4:
|
||||
version "3.1.4"
|
||||
resolved "https://registry.npmjs.org/array-includes/-/array-includes-3.1.4.tgz#f5b493162c760f3539631f005ba2bb46acb45ba9"
|
||||
integrity sha512-ZTNSQkmWumEbiHO2GF4GmWxYVTiQyJy2XOTa15sdQSrvKn7l+180egQMqlrMOUMCyLMD7pmyQe4mMDUT6Behrw==
|
||||
@@ -12064,6 +12069,11 @@ event-source-polyfill@1.0.25:
|
||||
resolved "https://registry.npmjs.org/event-source-polyfill/-/event-source-polyfill-1.0.25.tgz#d8bb7f99cb6f8119c2baf086d9f6ee0514b6d9c8"
|
||||
integrity sha512-hQxu6sN1Eq4JjoI7ITdQeGGUN193A2ra83qC0Ltm9I2UJVAten3OFVN6k5RX4YWeCS0BoC8xg/5czOCIHVosQg==
|
||||
|
||||
event-source-polyfill@^1.0.25:
|
||||
version "1.0.26"
|
||||
resolved "https://registry.npmjs.org/event-source-polyfill/-/event-source-polyfill-1.0.26.tgz#86c04d088ef078279168eefa028f928fec5059a4"
|
||||
integrity sha512-IwDLs9fUTcGAyacHBeS53T8wcEkDyDn0UP4tfQqJ4wQP8AyH0mszuQf2ULTylnpI0sMquzJ4usrNV7+uztwI9A==
|
||||
|
||||
event-stream@=3.3.4:
|
||||
version "3.3.4"
|
||||
resolved "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz#4ab4c9a0f5a54db9338b4c34d86bfce8f4b35571"
|
||||
@@ -16184,7 +16194,25 @@ jss@10.6.0, jss@^10.5.1:
|
||||
is-in-browser "^1.1.3"
|
||||
tiny-warning "^1.0.2"
|
||||
|
||||
"jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.2.1:
|
||||
jss@~10.8.2:
|
||||
version "10.8.2"
|
||||
resolved "https://registry.npmjs.org/jss/-/jss-10.8.2.tgz#4b2a30b094b924629a64928236017a52c7c97505"
|
||||
integrity sha512-FkoUNxI329CKQ9OQC8L72MBF9KPf5q8mIupAJ5twU7G7XREW7ahb+7jFfrjZ4iy1qvhx1HwIWUIvkZBDnKkEdQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.3.1"
|
||||
csstype "^3.0.2"
|
||||
is-in-browser "^1.1.3"
|
||||
tiny-warning "^1.0.2"
|
||||
|
||||
"jsx-ast-utils@^2.4.1 || ^3.0.0":
|
||||
version "3.2.0"
|
||||
resolved "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.2.0.tgz#41108d2cec408c3453c1bbe8a4aae9e1e2bd8f82"
|
||||
integrity sha512-EIsmt3O3ljsU6sot/J4E1zDRxfBNrhjyf/OKjlydwgEimQuznlM4Wv7U+ueONJMyEn1WRE0K8dhi3dVAXYT24Q==
|
||||
dependencies:
|
||||
array-includes "^3.1.2"
|
||||
object.assign "^4.1.2"
|
||||
|
||||
jsx-ast-utils@^3.2.1:
|
||||
version "3.2.1"
|
||||
resolved "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.2.1.tgz#720b97bfe7d901b927d87c3773637ae8ea48781b"
|
||||
integrity sha512-uP5vu8xfy2F9A6LGC22KO7e2/vGTS1MhP+18f++ZNlf0Ohaxbc9nIEwHAsejlJKyzfZzU5UIhe5ItYkitcZnZA==
|
||||
@@ -23708,6 +23736,7 @@ tdigest@^0.1.1:
|
||||
"@backstage/integration-react" "^1.0.1-next.1"
|
||||
"@backstage/plugin-catalog" "^1.1.0-next.1"
|
||||
"@backstage/plugin-techdocs" "^1.0.1-next.1"
|
||||
"@backstage/plugin-techdocs-react" "^0.0.0"
|
||||
"@backstage/test-utils" "^1.0.1-next.1"
|
||||
"@backstage/theme" "^0.2.15"
|
||||
"@material-ui/core" "^4.11.0"
|
||||
|
||||
Reference in New Issue
Block a user