From 794af0de8e5664130c6c9875cbc82296be16d0dc Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 9 Jun 2021 15:30:15 +0200 Subject: [PATCH] feat: more work with the nice API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Patrik Oldsberg Co-authored-by: Fredrik Adelöw Co-authored-by: Johan Haals Signed-off-by: blam --- .../src/extensions/pennywise.ts | 97 +++++++++++++++++++ .../components/EntityLayout/EntityLayout.tsx | 64 ++++++------ 2 files changed, 125 insertions(+), 36 deletions(-) create mode 100644 packages/core-plugin-api/src/extensions/pennywise.ts diff --git a/packages/core-plugin-api/src/extensions/pennywise.ts b/packages/core-plugin-api/src/extensions/pennywise.ts new file mode 100644 index 0000000000..4574bafb3b --- /dev/null +++ b/packages/core-plugin-api/src/extensions/pennywise.ts @@ -0,0 +1,97 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { getComponentData } from './componentData'; + +/** + * Returns an array of each component data value for a given key of each + * element in the entire react element tree starting at the provided children. + * + * - This was needed to grab the actual component data once we had narrowed down the set of children + */ +export const useCollectComponentData = ( + children: React.ReactNode, + componentDataKey: string, +) => { + const stack = [children]; + const found: T[] = []; + + while (stack.length) { + const current: React.ReactNode = stack.pop()!; + + React.Children.forEach(current, child => { + if (!React.isValidElement(child)) { + return; + } + + const data = getComponentData(child, componentDataKey); + if (data) { + found.push(data); + } + + if (child.props.children) { + stack.push(child.props.children); + } + }); + } + + return found; +}; + +/** + * Returns an array of all values of the children prop of each element with the entire + * react element tree that has component data for the given key. + * + * - this was needed to collect the children of ScaffolderFieldExtensions elements + */ +export const useCollectChildren = ( + component: React.ReactNode, + componentDataKey: string, +) => { + const stack = [component]; + const found: React.ReactNode[] = []; + + while (stack.length) { + const current: React.ReactNode = stack.pop()!; + + React.Children.forEach(current, child => { + if (!React.isValidElement(child)) { + return; + } + + if (child.props.children) { + if (getComponentData(child, componentDataKey)) { + found.push(child.props.children); + } + stack.push(child.props.children); + } + }); + } + + return found; +}; + +/** + * + * + * TODO: + * support: + * - entity layout route traversal + * - scaffolder field extension enumeration + * - FlatRoutes + * - Respecting feature flags + */ +export function useElementCollection(children: ReactNode) {} diff --git a/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx b/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx index dfd27631f8..68b254ec59 100644 --- a/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx +++ b/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx @@ -28,6 +28,10 @@ import { Page, Progress, RoutedTabs, + FeatureFlagsApi, + getComponentData, + featureFlagsApiRef, + useApi, } from '@backstage/core'; import { EntityContext, @@ -58,46 +62,14 @@ type SubRoute = { tabProps?: TabProps; }; +const dataKey = 'plugin.catalog.entityLayoutRoute'; + const Route: (props: SubRoute) => null = () => null; +attachComponentData(Route, dataKey, true); // This causes all mount points that are discovered within this route to use the path of the route itself attachComponentData(Route, 'core.gatherMountPoints', true); -function createSubRoutesFromChildren( - childrenProps: React.ReactNode, - entity: Entity | undefined, -): SubRoute[] { - // Directly comparing child.type with Route will not work with in - // combination with react-hot-loader in storybook - // https://github.com/gaearon/react-hot-loader/issues/304 - const routeType = ( - -
- - ).type; - - return Children.toArray(childrenProps).flatMap(child => { - if (!isValidElement(child)) { - return []; - } - - if (child.type === Fragment) { - return createSubRoutesFromChildren(child.props.children, entity); - } - - if (child.type !== routeType) { - throw new Error('Child of EntityLayout must be an EntityLayout.Route'); - } - - const { path, title, children, if: condition, tabProps } = child.props; - if (condition && entity && !condition(entity)) { - return []; - } - - return [{ path, title, children, tabProps }]; - }); -} - const EntityLayoutTitle = ({ entity, title, @@ -195,7 +167,27 @@ export const EntityLayout = ({ const { kind, namespace, name } = useEntityCompoundName(); const { entity, loading, error } = useContext(EntityContext); - const routes = createSubRoutesFromChildren(children, entity); + const routes = useElementCollection(children) + .findByComponentData({ + key: dataKey, + withStrictError: 'Child of EntityLayout must be an EntityLayout.Route', + }) + .listElements() // all nodes, element data, maintain structure or not? + .flatMap(({ props }) => { + if (props.condition && entity && !props.condition(entity)) { + return []; + } + + return [ + { + path: props.path, + title: props.title, + children: props.children, + tabProps: props.tabProps, + }, + ]; + }); + const { headerTitle, headerType } = headerProps( kind, namespace,