Review feedback 2: electric boogaloo
Co-authored-by: Otto Sichert <git@ottosichert.de> Co-authored-by: Anders Näsman <realandersn@users.noreply.github.com> Signed-off-by: Eric Peterson <ericpeterson@spotify.com>
This commit is contained in:
@@ -51,7 +51,7 @@ const getDataKeyByName = (name: string) => {
|
||||
* Create a TechDocs addon.
|
||||
* @alpha
|
||||
*/
|
||||
export function createTechDocsAddon<TComponentProps>(
|
||||
export function createTechDocsAddonExtension<TComponentProps>(
|
||||
options: TechDocsAddonOptions<TComponentProps>,
|
||||
): Extension<ComponentType<TComponentProps>> {
|
||||
const { name, component: TechDocsAddon } = options;
|
||||
@@ -67,7 +67,10 @@ export function createTechDocsAddon<TComponentProps>(
|
||||
});
|
||||
}
|
||||
|
||||
const getTechDocsAddonByName = (collection: ElementCollection, key: string) => {
|
||||
const getTechDocsAddonByName = (
|
||||
collection: ElementCollection,
|
||||
key: string,
|
||||
): JSX.Element | undefined => {
|
||||
return collection.selectByComponentData({ key }).getElements()[0];
|
||||
};
|
||||
|
||||
@@ -118,7 +121,7 @@ export const useTechDocsAddons = () => {
|
||||
);
|
||||
|
||||
const renderComponentsByLocation = useCallback(
|
||||
(location: TechDocsAddonLocations) => {
|
||||
(location: keyof typeof TechDocsAddonLocations) => {
|
||||
const data = options.filter(option => option.location === location);
|
||||
return data.length ? data.map(findAddonByData) : null;
|
||||
},
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
@@ -21,12 +21,9 @@ 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,
|
||||
TechDocsReaderPageProvider,
|
||||
} from '@backstage/plugin-techdocs';
|
||||
|
||||
import { useTechDocsReaderPage } from './context';
|
||||
import { techdocsApiRef } from './api';
|
||||
import { useTechDocsReaderPage, TechDocsReaderPageProvider } from './context';
|
||||
import { TechDocsMetadata } from './types';
|
||||
|
||||
const mockShadowRoot = () => {
|
||||
@@ -59,19 +56,19 @@ const techdocsApiMock = {
|
||||
};
|
||||
|
||||
const wrapper = ({
|
||||
entityName = {
|
||||
entityRef = {
|
||||
kind: mockEntityMetadata.kind,
|
||||
name: mockEntityMetadata.metadata.name,
|
||||
namespace: mockEntityMetadata.metadata.namespace!!,
|
||||
},
|
||||
children,
|
||||
}: {
|
||||
entityName?: CompoundEntityRef;
|
||||
entityRef?: CompoundEntityRef;
|
||||
children: React.ReactNode;
|
||||
}) => (
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<TestApiProvider apis={[[techdocsApiRef, techdocsApiMock]]}>
|
||||
<TechDocsReaderPageProvider entityName={entityName}>
|
||||
<TechDocsReaderPageProvider entityRef={entityRef}>
|
||||
{children}
|
||||
</TechDocsReaderPageProvider>
|
||||
</TestApiProvider>
|
||||
|
||||
@@ -14,20 +14,45 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Dispatch, SetStateAction, useContext } from 'react';
|
||||
import { AsyncState } from 'react-use/lib/useAsync';
|
||||
import React, {
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
useContext,
|
||||
useState,
|
||||
memo,
|
||||
ReactNode,
|
||||
} from 'react';
|
||||
import useAsync, { AsyncState } from 'react-use/lib/useAsync';
|
||||
|
||||
import { CompoundEntityRef } from '@backstage/catalog-model';
|
||||
import { createVersionedContext } from '@backstage/version-bridge';
|
||||
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>;
|
||||
entityName: CompoundEntityRef;
|
||||
entityRef: CompoundEntityRef;
|
||||
entityMetadata: AsyncState<TechDocsEntityMetadata>;
|
||||
shadowRoot?: ShadowRoot;
|
||||
setShadowRoot: Dispatch<SetStateAction<ShadowRoot | undefined>>;
|
||||
@@ -52,15 +77,80 @@ export const defaultTechDocsReaderPageValue: TechDocsReaderPageValue = {
|
||||
setShadowRoot: () => {},
|
||||
metadata: { loading: true },
|
||||
entityMetadata: { loading: true },
|
||||
entityName: { kind: '', name: '', namespace: '' },
|
||||
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;
|
||||
};
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
* A context to store the reader page state
|
||||
* @public
|
||||
*/
|
||||
export const TechDocsReaderPageContext = createVersionedContext<{
|
||||
1: TechDocsReaderPageValue;
|
||||
}>('techdocs-reader-page-context');
|
||||
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
|
||||
|
||||
@@ -22,16 +22,22 @@
|
||||
|
||||
export {
|
||||
useTechDocsAddons,
|
||||
createTechDocsAddon,
|
||||
createTechDocsAddonExtension,
|
||||
TechDocsAddons,
|
||||
TECHDOCS_ADDONS_WRAPPER_KEY,
|
||||
} from './addons';
|
||||
export { techdocsApiRef } from './api';
|
||||
export type { TechDocsApi } from './api';
|
||||
export {
|
||||
defaultTechDocsReaderPageValue,
|
||||
TechDocsReaderPageContext,
|
||||
TechDocsReaderPageProvider,
|
||||
useTechDocsReaderPage,
|
||||
} from './context';
|
||||
export type { TechDocsReaderPageValue } from './context';
|
||||
export type {
|
||||
TechDocsReaderPageProviderProps,
|
||||
TechDocsReaderPageProviderRenderFunction,
|
||||
TechDocsReaderPageValue,
|
||||
} from './context';
|
||||
export {
|
||||
useShadowRoot,
|
||||
useShadowRootElements,
|
||||
|
||||
@@ -40,34 +40,34 @@ export type TechDocsEntityMetadata = Entity & {
|
||||
* Locations for which TechDocs addons may be declared and rendered.
|
||||
* @alpha
|
||||
*/
|
||||
export enum TechDocsAddonLocations {
|
||||
export const TechDocsAddonLocations = Object.freeze({
|
||||
/**
|
||||
* These addons fill up the header from the right, on the same line as the
|
||||
* title.
|
||||
*/
|
||||
HEADER = 'header',
|
||||
Header: 'Header',
|
||||
|
||||
/**
|
||||
* These addons appear below the header and above all content; tooling addons
|
||||
* can be inserted for convenience.
|
||||
*/
|
||||
SUBHEADER = 'subheader',
|
||||
Subheader: 'Subheader',
|
||||
|
||||
/**
|
||||
* These addons appear left of the content and above the navigation.
|
||||
*/
|
||||
PRIMARY_SIDEBAR = 'primary sidebar',
|
||||
PrimarySidebar: 'PrimarySidebar',
|
||||
|
||||
/**
|
||||
* These addons appear right of the content and above the table of contents.
|
||||
*/
|
||||
SECONDARY_SIDEBAR = 'secondary sidebar',
|
||||
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',
|
||||
Content: 'Content',
|
||||
|
||||
/**
|
||||
* todo(backstage/community): This is a proposed virtual location which would
|
||||
@@ -97,8 +97,8 @@ export enum TechDocsAddonLocations {
|
||||
* addon, then replace them with component instances of the addon component,
|
||||
* passing any attributes from the tag as props to the component.
|
||||
*/
|
||||
// COMPONENT = 'component',
|
||||
}
|
||||
// Component: 'Component',
|
||||
} as const);
|
||||
|
||||
/**
|
||||
* Options for creating a TechDocs addon.
|
||||
@@ -106,6 +106,6 @@ export enum TechDocsAddonLocations {
|
||||
*/
|
||||
export type TechDocsAddonOptions<TAddonProps = {}> = {
|
||||
name: string;
|
||||
location: TechDocsAddonLocations;
|
||||
location: keyof typeof TechDocsAddonLocations;
|
||||
component: ComponentType<TAddonProps>;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user