chore: updated frontend-app-api

Signed-off-by: blam <ben@blam.sh>
This commit is contained in:
blam
2024-08-15 09:37:13 +02:00
parent 95f977f42c
commit 7df9a72154
13 changed files with 229 additions and 237 deletions
@@ -40,14 +40,8 @@ const baseOverrides = createExtensionOverrides({
namespace: 'app',
name: 'root',
attachTo: { id: 'app', input: 'root' },
output: {
element: coreExtensionData.reactElement,
},
factory() {
return {
element: <div>root</div>,
};
},
output: [coreExtensionData.reactElement],
factory: () => [coreExtensionData.reactElement(<div>root</div>)],
}),
],
});
@@ -18,51 +18,38 @@ import React from 'react';
import {
ExtensionBoundary,
coreExtensionData,
createApiExtension,
createComponentExtension,
createExtension,
createExtensionInput,
createThemeExtension,
createTranslationExtension,
IconBundleBlueprint,
ThemeBlueprint,
ApiBlueprint,
TranslationBlueprint,
} from '@backstage/frontend-plugin-api';
export const App = createExtension({
namespace: 'app',
attachTo: { id: 'root', input: 'default' }, // ignored
inputs: {
apis: createExtensionInput({
api: createApiExtension.factoryDataRef,
apis: createExtensionInput([ApiBlueprint.dataRefs.factory]),
themes: createExtensionInput([ThemeBlueprint.dataRefs.theme]),
components: createExtensionInput([
createComponentExtension.componentDataRef,
]),
translations: createExtensionInput([
TranslationBlueprint.dataRefs.translation,
]),
icons: createExtensionInput([IconBundleBlueprint.dataRefs.icons]),
root: createExtensionInput([coreExtensionData.reactElement], {
singleton: true,
}),
themes: createExtensionInput({
theme: createThemeExtension.themeDataRef,
}),
components: createExtensionInput({
component: createComponentExtension.componentDataRef,
}),
translations: createExtensionInput({
translation: createTranslationExtension.translationDataRef,
}),
icons: createExtensionInput({
icon: IconBundleBlueprint.dataRefs.icons,
}),
root: createExtensionInput(
{
element: coreExtensionData.reactElement,
},
{ singleton: true },
},
output: [coreExtensionData.reactElement],
factory: ({ node, inputs }) => [
coreExtensionData.reactElement(
<ExtensionBoundary node={node}>
{inputs.root.get(coreExtensionData.reactElement)}
</ExtensionBoundary>,
),
},
output: {
root: coreExtensionData.reactElement,
},
factory({ node, inputs }) {
return {
root: (
<ExtensionBoundary node={node}>
{inputs.root.output.element}
</ExtensionBoundary>
),
};
},
],
});
@@ -27,30 +27,20 @@ export const AppLayout = createExtension({
name: 'layout',
attachTo: { id: 'app/root', input: 'children' },
inputs: {
nav: createExtensionInput(
{
element: coreExtensionData.reactElement,
},
{ singleton: true },
nav: createExtensionInput([coreExtensionData.reactElement], {
singleton: true,
}),
content: createExtensionInput([coreExtensionData.reactElement], {
singleton: true,
}),
},
output: [coreExtensionData.reactElement],
factory: ({ inputs }) => [
coreExtensionData.reactElement(
<SidebarPage>
{inputs.nav.get(coreExtensionData.reactElement)}
{inputs.content.get(coreExtensionData.reactElement)}
</SidebarPage>,
),
content: createExtensionInput(
{
element: coreExtensionData.reactElement,
},
{ singleton: true },
),
},
output: {
element: coreExtensionData.reactElement,
},
factory({ inputs }) {
return {
element: (
<SidebarPage>
{inputs.nav.output.element}
{inputs.content.output.element}
</SidebarPage>
),
};
},
],
});
@@ -86,33 +86,27 @@ export const AppNav = createExtension({
name: 'nav',
attachTo: { id: 'app/layout', input: 'nav' },
inputs: {
items: createExtensionInput({
target: createNavItemExtension.targetDataRef,
items: createExtensionInput([createNavItemExtension.targetDataRef]),
logos: createExtensionInput([createNavLogoExtension.logoElementsDataRef], {
singleton: true,
optional: true,
}),
logos: createExtensionInput(
{
elements: createNavLogoExtension.logoElementsDataRef,
},
{
singleton: true,
optional: true,
},
},
output: [coreExtensionData.reactElement],
factory: ({ inputs }) => [
coreExtensionData.reactElement(
<Sidebar>
<SidebarLogo
{...inputs.logos?.get(createNavLogoExtension.logoElementsDataRef)}
/>
<SidebarDivider />
{inputs.items.map((item, index) => (
<SidebarNavItem
{...item.get(createNavItemExtension.targetDataRef)}
key={index}
/>
))}
</Sidebar>,
),
},
output: {
element: coreExtensionData.reactElement,
},
factory({ inputs }) {
return {
element: (
<Sidebar>
<SidebarLogo {...inputs.logos?.output.elements} />
<SidebarDivider />
{inputs.items.map((item, index) => (
<SidebarNavItem {...item.output.target} key={index} />
))}
</Sidebar>
),
};
},
],
});
@@ -23,12 +23,12 @@ import React, {
useState,
} from 'react';
import {
AppRootWrapperBlueprint,
RouterBlueprint,
SignInPageBlueprint,
coreExtensionData,
createAppRootWrapperExtension,
createExtension,
createExtensionInput,
createRouterExtension,
createSignInPageExtension,
} from '@backstage/frontend-plugin-api';
import {
IdentityApi,
@@ -48,52 +48,54 @@ export const AppRoot = createExtension({
name: 'root',
attachTo: { id: 'app', input: 'root' },
inputs: {
router: createExtensionInput(
{ component: createRouterExtension.componentDataRef },
{ singleton: true, optional: true },
),
signInPage: createExtensionInput(
{ component: createSignInPageExtension.componentDataRef },
{ singleton: true, optional: true },
),
children: createExtensionInput(
{ element: coreExtensionData.reactElement },
{ singleton: true },
),
elements: createExtensionInput({
element: coreExtensionData.reactElement,
router: createExtensionInput([RouterBlueprint.dataRefs.component], {
singleton: true,
optional: true,
}),
wrappers: createExtensionInput({
component: createAppRootWrapperExtension.componentDataRef,
signInPage: createExtensionInput([SignInPageBlueprint.dataRefs.component], {
singleton: true,
optional: true,
}),
children: createExtensionInput([coreExtensionData.reactElement], {
singleton: true,
}),
elements: createExtensionInput([coreExtensionData.reactElement]),
wrappers: createExtensionInput([
AppRootWrapperBlueprint.dataRefs.component,
]),
},
output: {
element: coreExtensionData.reactElement,
},
output: [coreExtensionData.reactElement],
factory({ inputs }) {
let content: React.ReactNode = (
<>
{inputs.elements.map(el => (
<Fragment key={el.node.spec.id}>{el.output.element}</Fragment>
<Fragment key={el.node.spec.id}>
{el.get(coreExtensionData.reactElement)}
</Fragment>
))}
{inputs.children.output.element}
{inputs.children.get(coreExtensionData.reactElement)}
</>
);
for (const wrapper of inputs.wrappers) {
content = <wrapper.output.component>{content}</wrapper.output.component>;
const Component = wrapper.get(AppRootWrapperBlueprint.dataRefs.component);
content = <Component>{content}</Component>;
}
return {
element: (
return [
coreExtensionData.reactElement(
<AppRouter
SignInPageComponent={inputs.signInPage?.output.component}
RouterComponent={inputs.router?.output.component}
SignInPageComponent={inputs.signInPage?.get(
SignInPageBlueprint.dataRefs.component,
)}
RouterComponent={inputs.router?.get(
RouterBlueprint.dataRefs.component,
)}
>
{content}
</AppRouter>
</AppRouter>,
),
};
];
},
});
@@ -29,15 +29,13 @@ export const AppRoutes = createExtension({
name: 'routes',
attachTo: { id: 'app/layout', input: 'content' },
inputs: {
routes: createExtensionInput({
path: coreExtensionData.routePath,
ref: coreExtensionData.routeRef.optional(),
element: coreExtensionData.reactElement,
}),
},
output: {
element: coreExtensionData.reactElement,
routes: createExtensionInput([
coreExtensionData.routePath,
coreExtensionData.routeRef.optional(),
coreExtensionData.reactElement,
]),
},
output: [coreExtensionData.reactElement],
factory({ inputs }) {
const Routes = () => {
const NotFoundErrorPage = useComponentRef(
@@ -46,8 +44,8 @@ export const AppRoutes = createExtension({
const element = useRoutes([
...inputs.routes.map(route => ({
path: `${route.output.path}/*`,
element: route.output.element,
path: `${route.get(coreExtensionData.routePath)}/*`,
element: route.get(coreExtensionData.reactElement),
})),
{
path: '*',
@@ -58,8 +56,6 @@ export const AppRoutes = createExtension({
return element;
};
return {
element: <Routes />,
};
return [coreExtensionData.reactElement(<Routes />)];
},
});
@@ -15,31 +15,36 @@
*/
import { AlertDisplay, OAuthRequestDialog } from '@backstage/core-components';
import {
createAppRootElementExtension,
createSchemaFromZod,
} from '@backstage/frontend-plugin-api';
import { AppRootElementBlueprint } from '@backstage/frontend-plugin-api';
import React from 'react';
export const oauthRequestDialogAppRootElement = createAppRootElementExtension({
export const oauthRequestDialogAppRootElement = AppRootElementBlueprint.make({
namespace: 'app',
name: 'oauth-request-dialog',
element: <OAuthRequestDialog />,
params: {
element: <OAuthRequestDialog />,
},
});
export const alertDisplayAppRootElement = createAppRootElementExtension({
namespace: 'app',
name: 'alert-display',
configSchema: createSchemaFromZod(z =>
z.object({
transientTimeoutMs: z.number().default(5000),
anchorOrigin: z
.object({
vertical: z.enum(['top', 'bottom']).default('top'),
horizontal: z.enum(['left', 'center', 'right']).default('center'),
})
.default({}),
}),
),
element: ({ config }) => <AlertDisplay {...config} />,
});
export const alertDisplayAppRootElement =
AppRootElementBlueprint.makeWithOverrides({
namespace: 'app',
name: 'alert-display',
config: {
schema: {
transientTimeoutMs: z => z.number().default(5000),
anchorOrigin: z =>
z
.object({
vertical: z.enum(['top', 'bottom']).default('top'),
horizontal: z.enum(['left', 'center', 'right']).default('center'),
})
.default({}),
},
},
factory: (originalFactory, { config }) => {
return originalFactory({
element: () => <AlertDisplay {...config} />,
});
},
});
@@ -21,24 +21,34 @@ import {
} from '@backstage/theme';
import DarkIcon from '@material-ui/icons/Brightness2';
import LightIcon from '@material-ui/icons/WbSunny';
import { createThemeExtension } from '@backstage/frontend-plugin-api';
import { ThemeBlueprint } from '@backstage/frontend-plugin-api';
export const LightTheme = createThemeExtension({
id: 'light',
title: 'Light Theme',
variant: 'light',
icon: <LightIcon />,
Provider: ({ children }) => (
<UnifiedThemeProvider theme={builtinThemes.light} children={children} />
),
export const LightTheme = ThemeBlueprint.make({
name: 'light',
params: {
theme: {
id: 'light',
title: 'Light Theme',
variant: 'light',
icon: <LightIcon />,
Provider: ({ children }) => (
<UnifiedThemeProvider theme={builtinThemes.light} children={children} />
),
},
},
});
export const DarkTheme = createThemeExtension({
id: 'dark',
title: 'Dark Theme',
variant: 'dark',
icon: <DarkIcon />,
Provider: ({ children }) => (
<UnifiedThemeProvider theme={builtinThemes.dark} children={children} />
),
export const DarkTheme = ThemeBlueprint.make({
name: 'dark',
params: {
theme: {
id: 'dark',
title: 'Dark Theme',
variant: 'dark',
icon: <DarkIcon />,
Provider: ({ children }) => (
<UnifiedThemeProvider theme={builtinThemes.dark} children={children} />
),
},
},
});
@@ -50,22 +50,23 @@ function createTestExtension(options: {
attachTo: options.parent
? { id: `test/${options.parent}`, input: 'children' }
: { id: 'app/routes', input: 'routes' },
output: {
element: coreExtensionData.reactElement,
path: coreExtensionData.routePath.optional(),
routeRef: coreExtensionData.routeRef.optional(),
},
output: [
coreExtensionData.reactElement,
coreExtensionData.routePath.optional(),
coreExtensionData.routeRef.optional(),
],
inputs: {
children: createExtensionInput({
element: coreExtensionData.reactElement,
}),
children: createExtensionInput([coreExtensionData.reactElement]),
},
factory() {
return {
path: options.path,
routeRef: options.routeRef,
element: React.createElement('div'),
};
*factory() {
if (options.path) {
yield coreExtensionData.routePath(options.path);
}
if (options.routeRef) {
yield coreExtensionData.routeRef(options.routeRef);
}
yield coreExtensionData.reactElement(React.createElement('div'));
},
});
}
@@ -25,8 +25,8 @@ import { createAppTree } from './createAppTree';
const extBase = {
id: 'test',
attachTo: { id: 'app', input: 'root' },
output: {},
factory: () => ({}),
output: [],
factory: () => [],
};
describe('createAppTree', () => {
@@ -60,8 +60,8 @@ describe('createAppTree', () => {
name: 'app',
attachTo: { id: 'app/routes', input: 'route' },
inputs: {},
output: {},
factory: () => ({}),
output: [],
factory: () => [],
}),
],
}),
@@ -23,8 +23,8 @@ const extension = resolveExtensionDefinition(
createExtension({
name: 'test',
attachTo: { id: 'nonexistent', input: 'nonexistent' },
output: {},
factory: () => ({}),
output: [],
factory: () => [],
}),
) as Extension<unknown, unknown>;
@@ -20,9 +20,9 @@ import {
coreExtensionData,
createExtension,
createExtensionOverrides,
createPageExtension,
PageBlueprint,
createFrontendPlugin,
createThemeExtension,
ThemeBlueprint,
} from '@backstage/frontend-plugin-api';
import { screen, waitFor } from '@testing-library/react';
import { CreateAppFeatureLoader, createApp } from './createApp';
@@ -47,11 +47,16 @@ describe('createApp', () => {
createFrontendPlugin({
id: 'test',
extensions: [
createThemeExtension({
id: 'derp',
title: 'Derp',
variant: 'dark',
Provider: () => <div>Derp</div>,
ThemeBlueprint.make({
name: 'derp',
params: {
theme: {
id: 'derp',
title: 'Derp',
variant: 'dark',
Provider: () => <div>Derp</div>,
},
},
}),
],
}),
@@ -71,18 +76,22 @@ describe('createApp', () => {
createFrontendPlugin({
id: duplicatedFeatureId,
extensions: [
createPageExtension({
defaultPath: '/',
loader: async () => <div>First Page</div>,
PageBlueprint.make({
params: {
defaultPath: '/',
loader: async () => <div>First Page</div>,
},
}),
],
}),
createFrontendPlugin({
id: duplicatedFeatureId,
extensions: [
createPageExtension({
defaultPath: '/',
loader: async () => <div>Last Page</div>,
PageBlueprint.make({
params: {
defaultPath: '/',
loader: async () => <div>Last Page</div>,
},
}),
],
}),
@@ -110,9 +119,11 @@ describe('createApp', () => {
createFrontendPlugin({
id: 'test',
extensions: [
createPageExtension({
defaultPath: '/',
loader: async () => <div>{config.getString('key')}</div>,
PageBlueprint.make({
params: {
defaultPath: '/',
loader: async () => <div>{config.getString('key')}</div>,
},
}),
],
}),
@@ -170,7 +181,7 @@ describe('createApp', () => {
createExtension({
name: 'first',
attachTo: { id: 'app', input: 'root' },
output: { element: coreExtensionData.reactElement },
output: [coreExtensionData.reactElement],
factory() {
const Component = () => {
const flagsApi = useApi(featureFlagsApiRef);
@@ -184,7 +195,7 @@ describe('createApp', () => {
</div>
);
};
return { element: <Component /> };
return [coreExtensionData.reactElement(<Component />)];
},
}),
],
@@ -197,8 +208,8 @@ describe('createApp', () => {
name: 'root',
attachTo: { id: 'app', input: 'root' },
disabled: true,
output: {},
factory: () => ({}),
output: [],
factory: () => [],
}),
],
}),
@@ -221,14 +232,16 @@ describe('createApp', () => {
createFrontendPlugin({
id: 'my-plugin',
extensions: [
createPageExtension({
defaultPath: '/',
loader: async () => {
const Component = () => {
appTreeApi = useApi(appTreeApiRef);
return <div>My Plugin Page</div>;
};
return <Component />;
PageBlueprint.make({
params: {
defaultPath: '/',
loader: async () => {
const Component = () => {
appTreeApi = useApi(appTreeApiRef);
return <div>My Plugin Page</div>;
};
return <Component />;
},
},
}),
],
@@ -250,7 +263,7 @@ describe('createApp', () => {
content [
<app/routes out=[core.reactElement]>
routes [
<page:my-plugin out=[core.routing.path, core.routing.ref, core.reactElement] />
<page:my-plugin out=[core.routing.path, core.reactElement] />
]
</app/routes>
]
@@ -17,18 +17,18 @@
import React, { JSX, ReactNode } from 'react';
import { ConfigReader } from '@backstage/config';
import {
ApiBlueprint,
AppTree,
appTreeApiRef,
componentsApiRef,
coreExtensionData,
createApiExtension,
createThemeExtension,
createTranslationExtension,
FrontendFeature,
IconBundleBlueprint,
iconsApiRef,
RouteResolutionApi,
routeResolutionApiRef,
ThemeBlueprint,
TranslationBlueprint,
} from '@backstage/frontend-plugin-api';
import { App } from '../extensions/App';
import { AppRoutes } from '../extensions/AppRoutes';
@@ -110,7 +110,9 @@ import { stringifyError } from '@backstage/errors';
import { icons as defaultIcons } from '../../../app-defaults/src/defaults';
import { getBasePath } from '../routing/getBasePath';
const DefaultApis = defaultApis.map(factory => createApiExtension({ factory }));
const DefaultApis = defaultApis.map(factory =>
ApiBlueprint.make({ namespace: factory.api.id, params: { factory } }),
);
export const builtinExtensions = [
App,
@@ -357,23 +359,21 @@ function createApiHolder(
const pluginApis =
tree.root.edges.attachments
.get('apis')
?.map(e => e.instance?.getData(createApiExtension.factoryDataRef))
?.map(e => e.instance?.getData(ApiBlueprint.dataRefs.factory))
.filter((x): x is AnyApiFactory => !!x) ?? [];
const themeExtensions =
tree.root.edges.attachments
.get('themes')
?.map(e => e.instance?.getData(createThemeExtension.themeDataRef))
?.map(e => e.instance?.getData(ThemeBlueprint.dataRefs.theme))
.filter((x): x is AppTheme => !!x) ?? [];
const translationResources =
tree.root.edges.attachments
.get('translations')
?.map(e =>
e.instance?.getData(createTranslationExtension.translationDataRef),
)
?.map(e => e.instance?.getData(TranslationBlueprint.dataRefs.translation))
.filter(
(x): x is typeof createTranslationExtension.translationDataRef.T => !!x,
(x): x is typeof TranslationBlueprint.dataRefs.translation.T => !!x,
) ?? [];
const extensionIcons = tree.root.edges.attachments