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:
Eric Peterson
2022-04-12 15:13:04 +02:00
parent a357c354af
commit 39a6eda01d
24 changed files with 271 additions and 245 deletions
+6 -3
View File
@@ -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;
},
+41
View File
@@ -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',
});
+5 -8
View File
@@ -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>
+100 -10
View File
@@ -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
+9 -3
View File
@@ -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,
+9 -9
View File
@@ -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>;
};