Merge pull request #7092 from backstage/rugvip/datawut
core-plugin-api: switch to using a plain string key for attaching component data
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/core-app-api': patch
|
||||
---
|
||||
|
||||
Removed deprecated internal functions.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/core-plugin-api': patch
|
||||
---
|
||||
|
||||
Migrated component data attachment method to have better compatibility with component proxies such as `react-hot-loader`.
|
||||
@@ -1,118 +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 { attachComponentData, getComponentData } from './componentData';
|
||||
|
||||
describe('elementData', () => {
|
||||
it('should attach a single piece of data', () => {
|
||||
const data = { foo: 'bar' };
|
||||
const Component = () => null;
|
||||
attachComponentData(Component, 'my-data', data);
|
||||
|
||||
const element = <Component />;
|
||||
expect(getComponentData(element, 'my-data')).toBe(data);
|
||||
});
|
||||
|
||||
it('should attach several distinct pieces of data', () => {
|
||||
const data1 = { foo: 'bar' };
|
||||
const data2 = { test: 'value' };
|
||||
const Component = () => null;
|
||||
attachComponentData(Component, 'my-data', data1);
|
||||
attachComponentData(Component, 'second', data2);
|
||||
|
||||
const element = <Component />;
|
||||
expect(getComponentData(element, 'my-data')).toBe(data1);
|
||||
expect(getComponentData(element, 'second')).toBe(data2);
|
||||
});
|
||||
|
||||
it('returns undefined for missing data', () => {
|
||||
const data = { foo: 'bar' };
|
||||
const Component1 = () => null;
|
||||
const Component2 = () => null;
|
||||
attachComponentData(Component2, 'my-data', data);
|
||||
|
||||
const element1 = <Component1 />;
|
||||
const element2 = <Component2 />;
|
||||
expect(getComponentData(element1, 'missing')).toBeUndefined();
|
||||
expect(getComponentData(element2, 'missing')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should throw when attempting to overwrite data', () => {
|
||||
const data = { foo: 'bar' };
|
||||
const MyComponent = () => null;
|
||||
attachComponentData(MyComponent, 'my-data', data);
|
||||
expect(() => attachComponentData(MyComponent, 'my-data', data)).toThrow(
|
||||
'Attempted to attach duplicate data "my-data" to component "MyComponent"',
|
||||
);
|
||||
});
|
||||
|
||||
describe('works across versions', () => {
|
||||
function getDataSymbol() {
|
||||
const Component = () => null;
|
||||
attachComponentData(Component, 'my-data', {});
|
||||
const [symbol] = Object.getOwnPropertySymbols(Component);
|
||||
return symbol;
|
||||
}
|
||||
|
||||
it('should should be able to get data from older versions', () => {
|
||||
const symbol = getDataSymbol();
|
||||
|
||||
const data = { foo: 'bar' };
|
||||
const Component = () => null;
|
||||
attachComponentData(Component, 'my-data', data);
|
||||
|
||||
const element = <Component />;
|
||||
expect((element as any).type[symbol].map.get('my-data')).toBe(data);
|
||||
});
|
||||
|
||||
it('should should be able to attach data for older versions', () => {
|
||||
const symbol = getDataSymbol();
|
||||
|
||||
const data = { foo: 'bar' };
|
||||
const Component = () => null;
|
||||
(Component as any)[symbol] = {
|
||||
map: new Map([['my-data', data]]),
|
||||
};
|
||||
|
||||
const element = <Component />;
|
||||
expect(getComponentData(element, 'my-data')).toBe(data);
|
||||
});
|
||||
|
||||
it('should be able to get data from newer versions', () => {
|
||||
const data = { foo: 'bar' };
|
||||
const Component = () => null;
|
||||
attachComponentData(Component, 'my-data', data);
|
||||
|
||||
const element = <Component />;
|
||||
const container = (global as any)[
|
||||
'__@backstage/component-data-store__'
|
||||
].get(element.type);
|
||||
expect(container.map.get('my-data')).toBe(data);
|
||||
});
|
||||
|
||||
it('should should be able to attach data for newer versions', () => {
|
||||
const data = { foo: 'bar' };
|
||||
const Component = () => null;
|
||||
(global as any)['__@backstage/component-data-store__'].set(Component, {
|
||||
map: new Map([['my-data', data]]),
|
||||
});
|
||||
|
||||
const element = <Component />;
|
||||
expect(getComponentData(element, 'my-data')).toBe(data);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,84 +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 { ComponentType, ReactNode } from 'react';
|
||||
import { getOrCreateGlobalSingleton } from '../lib/globalObject';
|
||||
|
||||
// TODO(Rugvip): Access via symbol is deprecated, remove once on 0.3.x
|
||||
const DATA_KEY = Symbol('backstage-component-data');
|
||||
|
||||
type ComponentWithData<P> = ComponentType<P> & {
|
||||
[DATA_KEY]?: DataContainer;
|
||||
};
|
||||
|
||||
type DataContainer = {
|
||||
map: Map<string, unknown>;
|
||||
};
|
||||
|
||||
type MaybeComponentNode = ReactNode & {
|
||||
type?: ComponentType<any> & { [DATA_KEY]?: DataContainer };
|
||||
};
|
||||
|
||||
// The store is bridged across versions using the global object
|
||||
const store = getOrCreateGlobalSingleton(
|
||||
'component-data-store',
|
||||
() => new WeakMap<ComponentType<any>, DataContainer>(),
|
||||
);
|
||||
|
||||
export function attachComponentData<P>(
|
||||
component: ComponentType<P>,
|
||||
type: string,
|
||||
data: unknown,
|
||||
) {
|
||||
const dataComponent = component as ComponentWithData<P>;
|
||||
|
||||
let container = store.get(component) || dataComponent[DATA_KEY];
|
||||
if (!container) {
|
||||
container = { map: new Map() };
|
||||
store.set(component, container);
|
||||
dataComponent[DATA_KEY] = container;
|
||||
}
|
||||
|
||||
if (container.map.has(type)) {
|
||||
const name = component.displayName || component.name;
|
||||
throw new Error(
|
||||
`Attempted to attach duplicate data "${type}" to component "${name}"`,
|
||||
);
|
||||
}
|
||||
|
||||
container.map.set(type, data);
|
||||
}
|
||||
|
||||
export function getComponentData<T>(
|
||||
node: ReactNode,
|
||||
type: string,
|
||||
): T | undefined {
|
||||
if (!node) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const component = (node as MaybeComponentNode).type;
|
||||
if (!component) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const container = store.get(component) || component[DATA_KEY];
|
||||
if (!container) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return container.map.get(type) as T | undefined;
|
||||
}
|
||||
@@ -1,54 +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 { Extension, RouteRef } from '@backstage/core-plugin-api';
|
||||
|
||||
type ComponentLoader<T> =
|
||||
| {
|
||||
lazy: () => Promise<T>;
|
||||
}
|
||||
| {
|
||||
sync: T;
|
||||
};
|
||||
|
||||
const ERROR_MESSAGE = 'Import this from @backstage/core-plugin-api';
|
||||
|
||||
/** @deprecated Import from @backstage/core-plugin-api instead */
|
||||
export function createRoutableExtension<
|
||||
T extends (props: any) => JSX.Element | null,
|
||||
>(_options: {
|
||||
component: () => Promise<T>;
|
||||
mountPoint: RouteRef;
|
||||
}): Extension<T> {
|
||||
throw new Error(ERROR_MESSAGE);
|
||||
}
|
||||
|
||||
/** @deprecated Import from @backstage/core-plugin-api instead */
|
||||
export function createComponentExtension<
|
||||
T extends (props: any) => JSX.Element | null,
|
||||
>(_options: { component: ComponentLoader<T> }): Extension<T> {
|
||||
throw new Error(ERROR_MESSAGE);
|
||||
}
|
||||
|
||||
/** @deprecated Import from @backstage/core-plugin-api instead */
|
||||
export function createReactExtension<
|
||||
T extends (props: any) => JSX.Element | null,
|
||||
>(_options: {
|
||||
component: ComponentLoader<T>;
|
||||
data?: Record<string, unknown>;
|
||||
}): Extension<T> {
|
||||
throw new Error(ERROR_MESSAGE);
|
||||
}
|
||||
@@ -1,22 +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.
|
||||
*/
|
||||
|
||||
export { attachComponentData, getComponentData } from './componentData';
|
||||
export {
|
||||
createReactExtension,
|
||||
createRoutableExtension,
|
||||
createComponentExtension,
|
||||
} from './extensions';
|
||||
@@ -14,8 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { BackstagePlugin } from '@backstage/core-plugin-api';
|
||||
import { getComponentData } from '../extensions';
|
||||
import { BackstagePlugin, getComponentData } from '@backstage/core-plugin-api';
|
||||
import { createCollector } from '../extensions/traversal';
|
||||
|
||||
export const pluginCollector = createCollector(
|
||||
|
||||
@@ -31,8 +31,8 @@ import {
|
||||
createRouteRef,
|
||||
createPlugin,
|
||||
RouteRef,
|
||||
attachComponentData,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { attachComponentData } from '../extensions';
|
||||
import { MemoryRouter, Routes, Route } from 'react-router-dom';
|
||||
|
||||
const MockComponent = ({ children }: PropsWithChildren<{ path?: string }>) => (
|
||||
|
||||
@@ -15,9 +15,8 @@
|
||||
*/
|
||||
|
||||
import { isValidElement, ReactElement, ReactNode } from 'react';
|
||||
import { RouteRef } from '@backstage/core-plugin-api';
|
||||
import { RouteRef, getComponentData } from '@backstage/core-plugin-api';
|
||||
import { BackstageRouteObject } from './types';
|
||||
import { getComponentData } from '../extensions';
|
||||
import { createCollector } from '../extensions/traversal';
|
||||
import { FeatureFlagged, FeatureFlaggedProps } from './FeatureFlagged';
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
import React, { ComponentType } from 'react';
|
||||
import { Button } from './Button';
|
||||
import { MemoryRouter, useLocation } from 'react-router-dom';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { createRouteRef, useRouteRef } from '@backstage/core-plugin-api';
|
||||
import {
|
||||
Divider,
|
||||
@@ -26,11 +26,7 @@ import {
|
||||
Typography,
|
||||
Button as MaterialButton,
|
||||
} from '@material-ui/core';
|
||||
// We don't want to export RoutingProvider from core-app-api, but it's way easier to
|
||||
// use here. This hack only works in storybook stories.
|
||||
// TODO: Export a nicer to user routing provider, perhaps from test-utils
|
||||
// eslint-disable-next-line monorepo/no-internal-import
|
||||
import { RoutingProvider } from '@backstage/core-app-api/src/routing/RoutingProvider';
|
||||
import { wrapInTestApp } from '@backstage/test-utils';
|
||||
|
||||
const routeRef = createRouteRef({
|
||||
id: 'storybook.test-route',
|
||||
@@ -45,36 +41,29 @@ export default {
|
||||
title: 'Inputs/Button',
|
||||
component: Button,
|
||||
decorators: [
|
||||
(Story: ComponentType<{}>) => (
|
||||
<>
|
||||
<Typography>
|
||||
A collection of buttons that should be used in the Backstage
|
||||
interface. These leverage the properties inherited from{' '}
|
||||
<Link href="https://material-ui.com/components/buttons/">
|
||||
Material-UI Button
|
||||
</Link>
|
||||
, but include an opinionated set that align to the Backstage design.
|
||||
</Typography>
|
||||
(Story: ComponentType<{}>) =>
|
||||
wrapInTestApp(
|
||||
<>
|
||||
<Typography>
|
||||
A collection of buttons that should be used in the Backstage
|
||||
interface. These leverage the properties inherited from{' '}
|
||||
<Link href="https://material-ui.com/components/buttons/">
|
||||
Material-UI Button
|
||||
</Link>
|
||||
, but include an opinionated set that align to the Backstage design.
|
||||
</Typography>
|
||||
|
||||
<Divider />
|
||||
<Divider />
|
||||
|
||||
<MemoryRouter>
|
||||
<RoutingProvider
|
||||
routeBindings={new Map()}
|
||||
routeObjects={[]}
|
||||
routeParents={new Map()}
|
||||
routePaths={new Map([[routeRef, '/hello']])}
|
||||
>
|
||||
<div>
|
||||
<div>
|
||||
<div>
|
||||
<Location />
|
||||
</div>
|
||||
<Story />
|
||||
<Location />
|
||||
</div>
|
||||
</RoutingProvider>
|
||||
</MemoryRouter>
|
||||
</>
|
||||
),
|
||||
<Story />
|
||||
</div>
|
||||
</>,
|
||||
{ mountedRoutes: { '/hello': routeRef } },
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -15,17 +15,9 @@
|
||||
*/
|
||||
import React, { ComponentType } from 'react';
|
||||
import { Link } from './Link';
|
||||
import {
|
||||
MemoryRouter,
|
||||
Route,
|
||||
useLocation,
|
||||
NavLink as RouterNavLink,
|
||||
} from 'react-router-dom';
|
||||
import { Route, useLocation, NavLink as RouterNavLink } from 'react-router-dom';
|
||||
import { createRouteRef, useRouteRef } from '@backstage/core-plugin-api';
|
||||
// We don't want to export RoutingProvider from core-app-api, but it's way easier to
|
||||
// use here. This hack only works in storybook stories.
|
||||
// eslint-disable-next-line monorepo/no-internal-import
|
||||
import { RoutingProvider } from '@backstage/core-app-api/src/routing/RoutingProvider';
|
||||
import { wrapInTestApp } from '@backstage/test-utils';
|
||||
|
||||
const routeRef = createRouteRef({
|
||||
id: 'storybook.test-route',
|
||||
@@ -40,23 +32,16 @@ export default {
|
||||
title: 'Navigation/Link',
|
||||
component: Link,
|
||||
decorators: [
|
||||
(Story: ComponentType<{}>) => (
|
||||
<MemoryRouter>
|
||||
<RoutingProvider
|
||||
routeBindings={new Map()}
|
||||
routeObjects={[]}
|
||||
routeParents={new Map()}
|
||||
routePaths={new Map([[routeRef, '/hello']])}
|
||||
>
|
||||
(Story: ComponentType<{}>) =>
|
||||
wrapInTestApp(
|
||||
<div>
|
||||
<div>
|
||||
<div>
|
||||
<Location />
|
||||
</div>
|
||||
<Story />
|
||||
<Location />
|
||||
</div>
|
||||
</RoutingProvider>
|
||||
</MemoryRouter>
|
||||
),
|
||||
<Story />
|
||||
</div>,
|
||||
{ mountedRoutes: { '/hello': routeRef } },
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
import React from 'react';
|
||||
import { attachComponentData, getComponentData } from './componentData';
|
||||
|
||||
describe('elementData', () => {
|
||||
describe('componentData', () => {
|
||||
it('should attach a single piece of data', () => {
|
||||
const data = { foo: 'bar' };
|
||||
const Component = () => null;
|
||||
@@ -59,4 +59,51 @@ describe('elementData', () => {
|
||||
'Attempted to attach duplicate data "my-data" to component "MyComponent"',
|
||||
);
|
||||
});
|
||||
|
||||
describe('works across versions', () => {
|
||||
it('should should be able to get data from newer versions', () => {
|
||||
const data = { foo: 'bar' };
|
||||
const Component = () => null;
|
||||
attachComponentData(Component, 'my-data', data);
|
||||
|
||||
const element = <Component />;
|
||||
expect((element as any).type.__backstage_data.map.get('my-data')).toBe(
|
||||
data,
|
||||
);
|
||||
});
|
||||
|
||||
it('should should be able to attach data for newer versions', () => {
|
||||
const data = { foo: 'bar' };
|
||||
const Component = () => null;
|
||||
(Component as any).__backstage_data = {
|
||||
map: new Map([['my-data', data]]),
|
||||
};
|
||||
|
||||
const element = <Component />;
|
||||
expect(getComponentData(element, 'my-data')).toBe(data);
|
||||
});
|
||||
|
||||
it('should be able to get data from older versions', () => {
|
||||
const data = { foo: 'bar' };
|
||||
const Component = () => null;
|
||||
attachComponentData(Component, 'my-data', data);
|
||||
|
||||
const element = <Component />;
|
||||
const container = (global as any)[
|
||||
'__@backstage/component-data-store__'
|
||||
].get(element.type);
|
||||
expect(container.map.get('my-data')).toBe(data);
|
||||
});
|
||||
|
||||
it('should should be able to attach data for older versions', () => {
|
||||
const data = { foo: 'bar' };
|
||||
const Component = () => null;
|
||||
(global as any)['__@backstage/component-data-store__'].set(Component, {
|
||||
map: new Map([['my-data', data]]),
|
||||
});
|
||||
|
||||
const element = <Component />;
|
||||
expect(getComponentData(element, 'my-data')).toBe(data);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,24 +21,42 @@ type DataContainer = {
|
||||
map: Map<string, unknown>;
|
||||
};
|
||||
|
||||
type MaybeComponentNode = ReactNode & {
|
||||
type?: ComponentType<any>;
|
||||
};
|
||||
|
||||
// The store is bridged across versions using the global object
|
||||
// This method of storing the component data was deprecated in September 2021, it
|
||||
// will be removed in the future for the reasons described below.
|
||||
const globalStore = getOrCreateGlobalSingleton(
|
||||
'component-data-store',
|
||||
() => new WeakMap<ComponentType<any>, DataContainer>(),
|
||||
);
|
||||
|
||||
// This key is used to attach component data to the component type (function or class)
|
||||
// itself. This method is used because it has better compatibility component wrappers
|
||||
// like react-hot-loader, as opposed to the WeakMap method or using a symbol.
|
||||
const componentDataKey = '__backstage_data';
|
||||
|
||||
type ComponentWithData = ComponentType<any> & {
|
||||
[componentDataKey]?: DataContainer;
|
||||
};
|
||||
|
||||
type MaybeComponentNode = ReactNode & {
|
||||
type?: ComponentWithData;
|
||||
};
|
||||
|
||||
export function attachComponentData<P>(
|
||||
component: ComponentType<P>,
|
||||
type: string,
|
||||
data: unknown,
|
||||
) {
|
||||
let container = globalStore.get(component);
|
||||
const dataComponent = component as ComponentWithData;
|
||||
|
||||
let container = dataComponent[componentDataKey] ?? globalStore.get(component);
|
||||
if (!container) {
|
||||
container = { map: new Map() };
|
||||
Object.defineProperty(dataComponent, componentDataKey, {
|
||||
enumerable: false,
|
||||
configurable: true,
|
||||
writable: false,
|
||||
value: container,
|
||||
});
|
||||
globalStore.set(component, container);
|
||||
}
|
||||
|
||||
@@ -65,7 +83,7 @@ export function getComponentData<T>(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const container = globalStore.get(component);
|
||||
const container = component[componentDataKey] ?? globalStore.get(component);
|
||||
if (!container) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user