Merge pull request #26215 from backstage/mob/app-it-up

NFS: Introduce the `@backstage/plugin-app` to house built in extensions
This commit is contained in:
Patrik Oldsberg
2024-08-27 13:36:21 +02:00
committed by GitHub
50 changed files with 1658 additions and 328 deletions
+8
View File
@@ -0,0 +1,8 @@
---
'@backstage/frontend-plugin-api': patch
'@backstage/frontend-test-utils': patch
'@backstage/frontend-app-api': patch
'@backstage/plugin-app': minor
---
Introduce the `@backstage/plugin-app` package to hold all of the built-in extensions for easy consumption and overriding.
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/plugin-app-backend': patch
'@backstage/plugin-app-node': patch
---
Fixing dependency metadata with the new `@backstage/plugin-app` package
@@ -495,7 +495,7 @@ The entity pages are typically defined in `packages/app/src/components/catalog`
New apps feature a built-in sidebar extension (`app/nav`) that will render all nav item extensions provided by plugins. This is a placeholder implementation and not intended as a long-term solution. In the future we will aim to provide a more flexible sidebar extension that allows for more customization out of the box.
Because the built-in sidebar is quite limited you may want to override the sidebar with your own custom implementation. To do so, use `createExtension` directly and refer to the [original sidebar implementation](https://github.com/backstage/backstage/blob/master/packages/frontend-app-api/src/extensions/AppNav.tsx). The following is an example of how to take your existing sidebar from the `Root` component that you typically find in `packages/app/src/components/Root.tsx`, and use it in an [extension override](../architecture/25-extension-overrides.md):
Because the built-in sidebar is quite limited you may want to override the sidebar with your own custom implementation. To do so, use `createExtension` directly and refer to the [original sidebar implementation](https://github.com/backstage/backstage/blob/master/plugins/app/src/extensions/AppNav.tsx). The following is an example of how to take your existing sidebar from the `Root` component that you typically find in `packages/app/src/components/Root.tsx`, and use it in an [extension override](../architecture/25-extension-overrides.md):
```tsx
const nav = createExtension({
+1
View File
@@ -24,6 +24,7 @@
"@backstage/frontend-plugin-api": "workspace:^",
"@backstage/integration-react": "workspace:^",
"@backstage/plugin-api-docs": "workspace:^",
"@backstage/plugin-app": "workspace:^",
"@backstage/plugin-app-visualizer": "workspace:^",
"@backstage/plugin-auth-react": "workspace:^",
"@backstage/plugin-catalog": "workspace:^",
@@ -262,7 +262,15 @@ describe('collectLegacyRoutes', () => {
component: () =>
Promise.resolve(() => {
const app = useApp();
return <div>plugins: {app.getPlugins().map(p => p.getId())}</div>;
return (
<div>
plugins:{' '}
{app
.getPlugins()
.map(p => p.getId())
.join(', ')}
</div>
);
}),
}),
);
@@ -276,7 +284,7 @@ describe('collectLegacyRoutes', () => {
render(createSpecializedApp({ features }).createRoot());
await expect(
screen.findByText('plugins: test'),
screen.findByText('plugins: app, test'),
).resolves.toBeInTheDocument();
});
@@ -52,7 +52,7 @@ describe('BackwardsCompatProvider', () => {
const app = useApp();
return (
<div data-testid="ctx">
plugins:
plugins:{' '}
{app
.getPlugins()
.map(p => p.getId())
@@ -74,7 +74,7 @@ describe('BackwardsCompatProvider', () => {
);
expect(screen.getByTestId('ctx').textContent).toMatchInlineSnapshot(`
"plugins:
"plugins: app
components: NotFoundErrorPage, BootErrorPage, Progress, Router, ErrorBoundaryFallback
icons: brokenImage, catalog, scaffolder, techdocs, search, chat, dashboard, docs, email, github, group, help, kind:api, kind:component, kind:domain, kind:group, kind:location, kind:system, kind:user, kind:resource, kind:template, user, warning"
`);
+1
View File
@@ -38,6 +38,7 @@
"@backstage/core-plugin-api": "workspace:^",
"@backstage/errors": "workspace:^",
"@backstage/frontend-plugin-api": "workspace:^",
"@backstage/plugin-app": "workspace:^",
"@backstage/theme": "workspace:^",
"@backstage/types": "workspace:^",
"@backstage/version-bridge": "workspace:^",
@@ -32,6 +32,6 @@ export const Root = createExtension({
replaces: [{ id: 'app', input: 'apis' }],
}),
},
output: [],
factory: () => [],
output: [coreExtensionData.reactElement],
factory: ({ inputs }) => inputs.app,
});
@@ -29,12 +29,15 @@ import {
createRouteRef,
} from '@backstage/frontend-plugin-api';
import { MockConfigApi, TestApiRegistry } from '@backstage/test-utils';
import appPlugin from '@backstage/plugin-app';
import { builtinExtensions } from '../wiring/createApp';
import { readAppExtensionsConfig } from '../tree/readAppExtensionsConfig';
import { resolveAppNodeSpecs } from '../tree/resolveAppNodeSpecs';
import { resolveAppTree } from '../tree/resolveAppTree';
import { instantiateAppNodeTree } from '../tree/instantiateAppNodeTree';
import { Root } from '../extensions/Root';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { resolveExtensionDefinition } from '../../../frontend-plugin-api/src/wiring/resolveExtensionDefinition';
const ref1 = createRouteRef();
const ref2 = createRouteRef();
@@ -83,12 +86,12 @@ function routeInfoFromExtensions(extensions: ExtensionDefinition[]) {
});
const tree = resolveAppTree(
'app',
'root',
resolveAppNodeSpecs({
features: [plugin],
builtinExtensions,
features: [appPlugin, plugin],
builtinExtensions: [resolveExtensionDefinition(Root)],
parameters: readAppExtensionsConfig(new MockConfigApi({})),
forbidden: new Set(['app']),
forbidden: new Set(['root']),
}),
);
@@ -29,6 +29,7 @@ import { CreateAppFeatureLoader, createApp } from './createApp';
import { MockConfigApi, renderWithEffects } from '@backstage/test-utils';
import React from 'react';
import { featureFlagsApiRef, useApi } from '@backstage/core-plugin-api';
import appPlugin from '@backstage/plugin-app';
describe('createApp', () => {
it('should allow themes to be installed', async () => {
@@ -136,7 +137,7 @@ describe('createApp', () => {
configLoader: async () => ({
config: new MockConfigApi({ key: 'config-value' }),
}),
features: [loader],
features: [appPlugin, loader],
});
await renderWithEffects(app.createRoot());
@@ -174,6 +175,7 @@ describe('createApp', () => {
const app = createApp({
configLoader: async () => ({ config: new MockConfigApi({}) }),
features: [
appPlugin,
createFrontendPlugin({
id: 'test',
featureFlags: [{ name: 'test-1' }],
@@ -255,13 +257,53 @@ describe('createApp', () => {
const { tree } = appTreeApi!.getTree();
expect(String(tree.root)).toMatchInlineSnapshot(`
"<root>
"<root out=[core.reactElement]>
apis [
<api:app/discovery out=[core.api.factory] />
<api:app/alert out=[core.api.factory] />
<api:app/analytics out=[core.api.factory] />
<api:app/error out=[core.api.factory] />
<api:app/storage out=[core.api.factory] />
<api:app/fetch out=[core.api.factory] />
<api:app/oauth-request out=[core.api.factory] />
<api:app/google-auth out=[core.api.factory] />
<api:app/microsoft-auth out=[core.api.factory] />
<api:app/github-auth out=[core.api.factory] />
<api:app/okta-auth out=[core.api.factory] />
<api:app/gitlab-auth out=[core.api.factory] />
<api:app/onelogin-auth out=[core.api.factory] />
<api:app/bitbucket-auth out=[core.api.factory] />
<api:app/bitbucket-server-auth out=[core.api.factory] />
<api:app/atlassian-auth out=[core.api.factory] />
<api:app/vmware-cloud-auth out=[core.api.factory] />
<api:app/permission out=[core.api.factory] />
<api:app/app-language out=[core.api.factory] />
<api:app/app-theme out=[core.api.factory]>
themes [
<theme:app/dark out=[core.theme.theme] />
<theme:app/light out=[core.theme.theme] />
]
</api:app/app-theme>
<api:app/components out=[core.api.factory]>
components [
<component:core.components.progress out=[core.component.component] />
<component:core.components.notFoundErrorPage out=[core.component.component] />
<component:core.components.errorBoundaryFallback out=[core.component.component] />
]
</api:app/components>
<api:app/icons out=[core.api.factory] />
<api:app/feature-flags out=[core.api.factory] />
<api:app/translations out=[core.api.factory] />
]
app [
<app out=[core.reactElement]>
root [
<app/root out=[core.reactElement]>
children [
<app/layout out=[core.reactElement]>
nav [
<app/nav out=[core.reactElement] />
]
content [
<app/routes out=[core.reactElement]>
routes [
@@ -269,9 +311,6 @@ describe('createApp', () => {
]
</app/routes>
]
nav [
<app/nav out=[core.reactElement] />
]
</app/layout>
]
elements [
@@ -282,43 +321,6 @@ describe('createApp', () => {
]
</app>
]
apis [
<api:app-theme out=[core.api.factory]>
themes [
<theme:app/light out=[core.theme.theme] />
<theme:app/dark out=[core.theme.theme] />
]
</api:app-theme>
<api:app-language out=[core.api.factory] />
<api:icons out=[core.api.factory] />
<api:translations out=[core.api.factory] />
<api:components out=[core.api.factory]>
components [
<component:core.components.progress out=[core.component.component] />
<component:core.components.errorBoundaryFallback out=[core.component.component] />
<component:core.components.notFoundErrorPage out=[core.component.component] />
]
</api:components>
<api:feature-flags out=[core.api.factory] />
<api:core.discovery out=[core.api.factory] />
<api:core.alert out=[core.api.factory] />
<api:core.analytics out=[core.api.factory] />
<api:core.error out=[core.api.factory] />
<api:core.storage out=[core.api.factory] />
<api:core.fetch out=[core.api.factory] />
<api:core.oauthrequest out=[core.api.factory] />
<api:core.auth.google out=[core.api.factory] />
<api:core.auth.microsoft out=[core.api.factory] />
<api:core.auth.github out=[core.api.factory] />
<api:core.auth.okta out=[core.api.factory] />
<api:core.auth.gitlab out=[core.api.factory] />
<api:core.auth.onelogin out=[core.api.factory] />
<api:core.auth.bitbucket out=[core.api.factory] />
<api:core.auth.bitbucket-server out=[core.api.factory] />
<api:core.auth.atlassian out=[core.api.factory] />
<api:core.auth.vmware-cloud out=[core.api.factory] />
<api:plugin.permission.api out=[core.api.factory] />
]
</root>"
`);
});
@@ -354,4 +356,27 @@ describe('createApp', () => {
expect(screen.queryByText('Custom loading message')).toBeNull();
});
it('should allow overriding the app plugin', async () => {
const app = createApp({
configLoader: () => new Promise(() => {}),
features: [
appPlugin.withOverrides({
extensions: [
appPlugin.getExtension('app/root').override({
factory: () => [
coreExtensionData.reactElement(
<div>Custom app root element</div>,
),
],
}),
],
}),
],
});
await renderWithEffects(app.createRoot());
expect(screen.queryByText('Custom app root element')).toBeNull();
});
});
@@ -22,7 +22,6 @@ import {
AppTreeApi,
appTreeApiRef,
coreExtensionData,
ExtensionDefinition,
FrontendFeature,
RouteRef,
ExternalRouteRef,
@@ -34,10 +33,7 @@ import {
createApiFactory,
routeResolutionApiRef,
} from '@backstage/frontend-plugin-api';
import { App } from '../extensions/App';
import { AppRoutes } from '../extensions/AppRoutes';
import { AppLayout } from '../extensions/AppLayout';
import { AppNav } from '../extensions/AppNav';
import {
AnyApiFactory,
ApiHolder,
@@ -45,62 +41,31 @@ import {
configApiRef,
featureFlagsApiRef,
identityApiRef,
errorApiRef,
discoveryApiRef,
fetchApiRef,
} from '@backstage/core-plugin-api';
import { getAvailableFeatures } from './discovery';
import {
ApiFactoryRegistry,
ApiProvider,
ApiResolver,
} from '@backstage/core-app-api';
import { ApiFactoryRegistry, ApiResolver } from '@backstage/core-app-api';
// TODO: Get rid of all of these
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { isProtectedApp } from '../../../core-app-api/src/app/isProtectedApp';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { AppThemeProvider } from '../../../core-app-api/src/app/AppThemeProvider';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { AppIdentityProxy } from '../../../core-app-api/src/apis/implementations/IdentityApi/AppIdentityProxy';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { defaultConfigLoaderSync } from '../../../core-app-api/src/app/defaultConfigLoader';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { overrideBaseUrlConfigs } from '../../../core-app-api/src/app/overrideBaseUrlConfigs';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { resolveExtensionDefinition } from '../../../frontend-plugin-api/src/wiring/resolveExtensionDefinition';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { apis as defaultApis } from '../../../app-defaults/src/defaults';
import {
oauthRequestDialogAppRootElement,
alertDisplayAppRootElement,
} from '../extensions/elements';
import { extractRouteInfoFromAppNode } from '../routing/extractRouteInfoFromAppNode';
import { CreateAppRouteBinder } from '../routing';
import { RouteResolver } from '../routing/RouteResolver';
import { resolveRouteBindings } from '../routing/resolveRouteBindings';
import { collectRouteIds } from '../routing/collectRouteIds';
import {
DefaultProgressComponent,
DefaultErrorBoundaryComponent,
DefaultNotFoundErrorPageComponent,
} from '../extensions/components';
import { InternalAppContext } from './InternalAppContext';
import { AppRoot } from '../extensions/AppRoot';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { toInternalBackstagePlugin } from '../../../frontend-plugin-api/src/wiring/createFrontendPlugin';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { toInternalExtensionOverrides } from '../../../frontend-plugin-api/src/wiring/createExtensionOverrides';
import { stringifyError } from '@backstage/errors';
import { getBasePath } from '../routing/getBasePath';
import { AppThemeApi, DarkTheme, LightTheme } from '../extensions/AppThemeApi';
import { IconsApi } from '../extensions/IconsApi';
import { TranslationsApi } from '../extensions/TranslationsApi';
import { ComponentsApi } from '../extensions/ComponentsApi';
import { AppLanguageApi } from '../extensions/AppLanguageApi';
import { FeatureFlagsApi } from '../extensions/FeatureFlagsApi';
import { Root } from '../extensions/Root';
import { resolveAppTree } from '../tree/resolveAppTree';
import { resolveAppNodeSpecs } from '../tree/resolveAppNodeSpecs';
@@ -108,35 +73,10 @@ import { readAppExtensionsConfig } from '../tree/readAppExtensionsConfig';
import { instantiateAppNodeTree } from '../tree/instantiateAppNodeTree';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { ApiRegistry } from '../../../core-app-api/src/apis/system/ApiRegistry';
const DefaultApis = defaultApis.map(factory =>
ApiBlueprint.make({ namespace: factory.api.id, params: { factory } }),
);
export const builtinExtensions = (
[
Root,
App,
AppRoot,
AppRoutes,
AppNav,
AppLayout,
DefaultProgressComponent,
DefaultErrorBoundaryComponent,
DefaultNotFoundErrorPageComponent,
LightTheme,
DarkTheme,
oauthRequestDialogAppRootElement,
alertDisplayAppRootElement,
AppThemeApi,
AppLanguageApi,
IconsApi,
TranslationsApi,
ComponentsApi,
FeatureFlagsApi,
...DefaultApis,
] as ExtensionDefinition[]
).map(def => resolveExtensionDefinition(def));
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { AppIdentityProxy } from '../../../core-app-api/src/apis/implementations/IdentityApi/AppIdentityProxy';
import { BackstageRouteObject } from '../routing/types';
import appPlugin from '@backstage/plugin-app';
function deduplicateFeatures(
allFeatures: FrontendFeature[],
@@ -271,6 +211,7 @@ class AppTreeApiProxy implements AppTreeApi {
// Helps delay callers from reaching out to the API before the app tree has been materialized
class RouteResolutionApiProxy implements RouteResolutionApi {
#delegate: RouteResolutionApi | undefined;
#routeObjects: BackstageRouteObject[] | undefined;
constructor(
private readonly tree: AppTree,
@@ -307,9 +248,14 @@ class RouteResolutionApiProxy implements RouteResolutionApi {
this.routeBindings,
this.basePath,
);
this.#routeObjects = routeInfo.routeObjects;
return routeInfo;
}
getRouteObjects() {
return this.#routeObjects;
}
}
/**
@@ -324,24 +270,23 @@ export function createSpecializedApp(options?: {
bindRoutes?(context: { bind: CreateAppRouteBinder }): void;
}): { createRoot(): JSX.Element } {
const {
features: duplicatedFeatures = [],
features: featuresWithoutApp = [],
config = new ConfigReader({}, 'empty-config'),
} = options ?? {};
const features = deduplicateFeatures(duplicatedFeatures);
const features = deduplicateFeatures([appPlugin, ...featuresWithoutApp]);
const tree = resolveAppTree(
'root',
resolveAppNodeSpecs({
features,
builtinExtensions,
builtinExtensions: [resolveExtensionDefinition(Root)],
parameters: readAppExtensionsConfig(config),
forbidden: new Set(['root']),
}),
);
const factories = createApiFactories({ tree });
const appTreeApi = new AppTreeApiProxy(tree);
const routeResolutionApi = new RouteResolutionApiProxy(
tree,
@@ -364,29 +309,12 @@ export function createSpecializedApp(options?: {
],
});
for (const appNode of tree.root.edges.attachments.get('app') ?? []) {
instantiateAppNodeTree(appNode, apiHolder);
}
// Now instantiate the entire tree, which will skip anything that's already been instantiated
instantiateAppNodeTree(tree.root, apiHolder);
const routeInfo = routeResolutionApi.initialize();
routeResolutionApi.initialize();
appTreeApi.initialize();
if (isProtectedApp()) {
const discoveryApi = apiHolder.get(discoveryApiRef);
const errorApi = apiHolder.get(errorApiRef);
const fetchApi = apiHolder.get(fetchApiRef);
if (!discoveryApi || !errorApi || !fetchApi) {
throw new Error(
'App is running in protected mode but missing required APIs',
);
}
appIdentityProxy.enableCookieAuth({
discoveryApi,
errorApi,
fetchApi,
});
}
const featureFlagApi = apiHolder.get(featureFlagsApiRef);
if (featureFlagApi) {
for (const feature of features) {
@@ -406,21 +334,9 @@ export function createSpecializedApp(options?: {
}
}
const rootEl = tree.root.edges.attachments
.get('app')![0]
.instance!.getData(coreExtensionData.reactElement);
const rootEl = tree.root.instance!.getData(coreExtensionData.reactElement);
const AppComponent = () => (
<ApiProvider apis={apiHolder}>
<AppThemeProvider>
<InternalAppContext.Provider
value={{ appIdentityProxy, routeObjects: routeInfo.routeObjects }}
>
{rootEl}
</InternalAppContext.Provider>
</AppThemeProvider>
</ApiProvider>
);
const AppComponent = () => rootEl;
return {
createRoot() {
@@ -16,15 +16,13 @@
import React from 'react';
import { AppRootWrapperBlueprint } from './AppRootWrapperBlueprint';
import { render, screen, waitFor } from '@testing-library/react';
import { screen, waitFor } from '@testing-library/react';
import {
coreExtensionData,
createExtension,
createExtensionInput,
createFrontendPlugin,
} from '../wiring';
import { createSpecializedApp } from '@backstage/frontend-app-api';
import { MockConfigApi } from '@backstage/test-utils';
import { renderInTestApp } from '@backstage/frontend-test-utils';
describe('AppRootWrapperBlueprint', () => {
it('should return an extension with sensible defaults', () => {
@@ -61,27 +59,20 @@ describe('AppRootWrapperBlueprint', () => {
it('should render the simple component wrapper', async () => {
const extension = AppRootWrapperBlueprint.make({
name: 'test',
params: {
Component: () => <div>Hello</div>,
},
});
const app = createSpecializedApp({
features: [
createFrontendPlugin({
id: 'test',
extensions: [extension],
}),
],
});
render(app.createRoot());
renderInTestApp(<div />, { extensions: [extension] });
await waitFor(() => expect(screen.getByText('Hello')).toBeInTheDocument());
});
it('should render the complex component wrapper', async () => {
const extension = AppRootWrapperBlueprint.makeWithOverrides({
name: 'test',
config: {
schema: {
name: z => z.string(),
@@ -104,24 +95,17 @@ describe('AppRootWrapperBlueprint', () => {
},
});
const app = createSpecializedApp({
features: [
createFrontendPlugin({
id: 'test',
extensions: [
extension,
createExtension({
name: 'test-child',
attachTo: { id: 'app-root-wrapper:test', input: 'children' },
output: [coreExtensionData.reactElement],
factory: () => [
coreExtensionData.reactElement(<div>Its Me</div>),
],
}),
],
renderInTestApp(<div />, {
extensions: [
extension,
createExtension({
name: 'test-child',
attachTo: { id: 'app-root-wrapper:test', input: 'children' },
output: [coreExtensionData.reactElement],
factory: () => [coreExtensionData.reactElement(<div>Its Me</div>)],
}),
],
config: new MockConfigApi({
config: {
app: {
extensions: [
{
@@ -129,11 +113,9 @@ describe('AppRootWrapperBlueprint', () => {
},
],
},
}),
},
});
render(app.createRoot());
await waitFor(() => {
expect(screen.getByTestId('Robin-1')).toBeInTheDocument();
expect(screen.getByText('Its Me')).toBeInTheDocument();
@@ -25,7 +25,7 @@ const iconsDataRef = createExtensionDataRef<{
export const IconBundleBlueprint = createExtensionBlueprint({
kind: 'icon-bundle',
namespace: 'app',
attachTo: { id: 'api:icons', input: 'icons' },
attachTo: { id: 'api:app/icons', input: 'icons' },
output: [iconsDataRef],
config: {
schema: {
@@ -17,15 +17,12 @@ import React from 'react';
import { RouterBlueprint } from './RouterBlueprint';
import { MemoryRouter } from 'react-router-dom';
import { render, waitFor } from '@testing-library/react';
import { createSpecializedApp } from '@backstage/frontend-app-api';
import {
coreExtensionData,
createExtension,
createExtensionInput,
createExtensionOverrides,
} from '../wiring';
import { MockConfigApi } from '@backstage/test-utils';
import { PageBlueprint } from './PageBlueprint';
import { createExtensionTester } from '@backstage/frontend-test-utils';
describe('RouterBlueprint', () => {
it('should return an extension when calling make with sensible defaults', () => {
@@ -72,24 +69,14 @@ describe('RouterBlueprint', () => {
},
});
const app = createSpecializedApp({
features: [
createExtensionOverrides({
extensions: [
extension,
PageBlueprint.make({
namespace: 'test',
params: {
defaultPath: '/',
loader: async () => <div data-testid="test-contents" />,
},
}),
],
}),
],
});
const tester = createExtensionTester(extension);
const Component = tester.get(RouterBlueprint.dataRefs.component);
const { getByTestId } = render(app.createRoot());
const { getByTestId } = render(
<Component>
<div data-testid="test-contents" />
</Component>,
);
await waitFor(() => {
expect(getByTestId('test-contents')).toBeInTheDocument();
@@ -124,44 +111,28 @@ describe('RouterBlueprint', () => {
},
});
const app = createSpecializedApp({
features: [
createExtensionOverrides({
extensions: [
extension,
createExtension({
namespace: 'test',
attachTo: {
id: 'app-router-component:test/test',
input: 'children',
},
output: [coreExtensionData.reactElement],
*factory() {
yield coreExtensionData.reactElement(<div />);
},
}),
PageBlueprint.make({
namespace: 'test',
params: {
defaultPath: '/',
loader: async () => <div data-testid="test-contents" />,
},
}),
],
}),
],
config: new MockConfigApi({
app: {
extensions: [
{
'app-router-component:test/test': { config: { name: 'Robin' } },
},
],
const tester = createExtensionTester(extension, {
config: { name: 'Robin' },
}).add(
createExtension({
namespace: 'test',
attachTo: {
id: 'app-router-component:test/test',
input: 'children',
},
output: [coreExtensionData.reactElement],
*factory() {
yield coreExtensionData.reactElement(<div />);
},
}),
});
);
const Component = tester.get(RouterBlueprint.dataRefs.component);
const { getByTestId } = render(app.createRoot());
const { getByTestId } = render(
<Component>
<div data-testid="test-contents" />
</Component>,
);
await waitFor(() => {
expect(getByTestId('test-contents')).toBeInTheDocument();
@@ -33,7 +33,7 @@ describe('ThemeBlueprint', () => {
"$$type": "@backstage/ExtensionDefinition",
"T": undefined,
"attachTo": {
"id": "api:app-theme",
"id": "api:app/app-theme",
"input": "themes",
},
"configSchema": undefined,
@@ -29,7 +29,7 @@ const themeDataRef = createExtensionDataRef<AppTheme>().with({
export const ThemeBlueprint = createExtensionBlueprint({
kind: 'theme',
namespace: 'app',
attachTo: { id: 'api:app-theme', input: 'themes' },
attachTo: { id: 'api:app/app-theme', input: 'themes' },
output: [themeDataRef],
dataRefs: {
theme: themeDataRef,
@@ -48,7 +48,7 @@ describe('TranslationBlueprint', () => {
"$$type": "@backstage/ExtensionDefinition",
"T": undefined,
"attachTo": {
"id": "api:translations",
"id": "api:app/translations",
"input": "translations",
},
"configSchema": undefined,
@@ -28,7 +28,7 @@ const translationDataRef = createExtensionDataRef<
*/
export const TranslationBlueprint = createExtensionBlueprint({
kind: 'translation',
attachTo: { id: 'api:translations', input: 'translations' },
attachTo: { id: 'api:app/translations', input: 'translations' },
output: [translationDataRef],
dataRefs: {
translation: translationDataRef,
@@ -35,7 +35,7 @@ export function createComponentExtension<TProps extends {}>(options: {
kind: 'component',
namespace: options.ref.id,
name: options.name,
attachTo: { id: 'api:components', input: 'components' },
attachTo: { id: 'api:app/components', input: 'components' },
disabled: options.disabled,
output: [createComponentExtension.componentDataRef],
factory() {
@@ -127,7 +127,7 @@ function createTestAppRoot({
config: JsonObject;
}) {
return createApp({
features,
features: [...features],
configLoader: async () => ({ config: new MockConfigApi(config) }),
}).createRoot();
}
@@ -34,6 +34,7 @@
"@backstage/config": "workspace:^",
"@backstage/frontend-app-api": "workspace:^",
"@backstage/frontend-plugin-api": "workspace:^",
"@backstage/plugin-app": "workspace:^",
"@backstage/test-utils": "workspace:^",
"@backstage/types": "workspace:^"
},
@@ -27,12 +27,12 @@ import {
coreExtensionData,
RouteRef,
useRouteRef,
createExtensionInput,
IconComponent,
RouterBlueprint,
NavItemBlueprint,
FrontendFeature,
} from '@backstage/frontend-plugin-api';
import appPlugin from '@backstage/plugin-app';
/**
* Options to customize the behavior of the test app.
@@ -92,38 +92,36 @@ const NavItem = (props: {
);
};
export const TestAppNavExtension = createExtension({
namespace: 'app',
name: 'nav',
attachTo: { id: 'app/layout', input: 'nav' },
inputs: {
items: createExtensionInput([NavItemBlueprint.dataRefs.target]),
},
output: [coreExtensionData.reactElement],
factory({ inputs }) {
return [
coreExtensionData.reactElement(
<nav>
<ul>
{inputs.items.map((item, index) => {
const { icon, title, routeRef } = item.get(
NavItemBlueprint.dataRefs.target,
);
const appPluginOverride = appPlugin.withOverrides({
extensions: [
appPlugin.getExtension('app/nav').override({
output: [coreExtensionData.reactElement],
factory(_originalFactory, { inputs }) {
return [
coreExtensionData.reactElement(
<nav>
<ul>
{inputs.items.map((item, index) => {
const { icon, title, routeRef } = item.get(
NavItemBlueprint.dataRefs.target,
);
return (
<NavItem
key={index}
icon={icon}
title={title}
routeRef={routeRef}
/>
);
})}
</ul>
</nav>,
),
];
},
return (
<NavItem
key={index}
icon={icon}
title={title}
routeRef={routeRef}
/>
);
})}
</ul>
</nav>,
),
];
},
}),
],
});
/**
@@ -152,7 +150,6 @@ export function renderInTestApp(
Component: ({ children }) => <MemoryRouter>{children}</MemoryRouter>,
},
}),
TestAppNavExtension,
];
if (options?.mountedRoutes) {
@@ -183,6 +180,7 @@ export function renderInTestApp(
}
const features: FrontendFeature[] = [
appPluginOverride,
createExtensionOverrides({
extensions,
}),
+1
View File
@@ -6,6 +6,7 @@
"role": "backend-plugin",
"pluginId": "app",
"pluginPackages": [
"@backstage/plugin-app",
"@backstage/plugin-app-backend",
"@backstage/plugin-app-node"
]
+1
View File
@@ -6,6 +6,7 @@
"role": "node-library",
"pluginId": "app",
"pluginPackages": [
"@backstage/plugin-app",
"@backstage/plugin-app-backend",
"@backstage/plugin-app-node"
]
+1
View File
@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
+9
View File
@@ -0,0 +1,9 @@
# app
Welcome to the app plugin!
_This plugin was created through the Backstage CLI_
## Getting started
Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/app](http://localhost:3000/app).
+681
View File
@@ -0,0 +1,681 @@
## API Report File for "@backstage/plugin-app"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
/// <reference types="react" />
import { AnyApiFactory } from '@backstage/frontend-plugin-api';
import { AnyExtensionDataRef } from '@backstage/frontend-plugin-api';
import { AnyRouteRefParams } from '@backstage/frontend-plugin-api';
import { AppTheme } from '@backstage/frontend-plugin-api';
import { BackstagePlugin } from '@backstage/frontend-plugin-api';
import { ComponentRef } from '@backstage/frontend-plugin-api';
import { ComponentType } from 'react';
import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api';
import { ExtensionDefinition } from '@backstage/frontend-plugin-api';
import { ExtensionInput } from '@backstage/frontend-plugin-api';
import { IconComponent } from '@backstage/core-plugin-api';
import { IconComponent as IconComponent_2 } from '@backstage/frontend-plugin-api';
import { JSX as JSX_2 } from 'react';
import { ReactNode } from 'react';
import { RouteRef } from '@backstage/frontend-plugin-api';
import { SignInPageProps } from '@backstage/core-plugin-api';
import { TranslationMessages } from '@backstage/frontend-plugin-api';
import { TranslationResource } from '@backstage/frontend-plugin-api';
// @public (undocumented)
const appPlugin: BackstagePlugin<
{},
{},
{
[x: `component:app/${string}`]: ExtensionDefinition<{
config: {};
configInput: {};
output: ConfigurableExtensionDataRef<
{
ref: ComponentRef;
impl: ComponentType;
},
'core.component.component',
{}
>;
inputs: {
[x: string]: ExtensionInput<
AnyExtensionDataRef,
{
optional: boolean;
singleton: boolean;
}
>;
};
kind: 'component';
namespace: string;
name: string;
}>;
app: ExtensionDefinition<{
config: {};
configInput: {};
output: ConfigurableExtensionDataRef<
JSX_2.Element,
'core.reactElement',
{}
>;
inputs: {
root: ExtensionInput<
ConfigurableExtensionDataRef<JSX_2.Element, 'core.reactElement', {}>,
{
singleton: true;
optional: false;
}
>;
};
kind: undefined;
namespace: 'app';
name: undefined;
}>;
'api:app/app-language': ExtensionDefinition<{
kind: 'api';
namespace: undefined;
name: 'app-language';
config: {};
configInput: {};
output: ConfigurableExtensionDataRef<
AnyApiFactory,
'core.api.factory',
{}
>;
inputs: {};
}>;
'app/layout': ExtensionDefinition<{
config: {};
configInput: {};
output: ConfigurableExtensionDataRef<
JSX_2.Element,
'core.reactElement',
{}
>;
inputs: {
nav: ExtensionInput<
ConfigurableExtensionDataRef<JSX_2.Element, 'core.reactElement', {}>,
{
singleton: true;
optional: false;
}
>;
content: ExtensionInput<
ConfigurableExtensionDataRef<JSX_2.Element, 'core.reactElement', {}>,
{
singleton: true;
optional: false;
}
>;
};
kind: undefined;
namespace: 'app';
name: 'layout';
}>;
'app/nav': ExtensionDefinition<{
config: {};
configInput: {};
output: ConfigurableExtensionDataRef<
JSX_2.Element,
'core.reactElement',
{}
>;
inputs: {
items: ExtensionInput<
ConfigurableExtensionDataRef<
{
title: string;
icon: IconComponent;
routeRef: RouteRef<undefined>;
},
'core.nav-item.target',
{}
>,
{
singleton: false;
optional: false;
}
>;
logos: ExtensionInput<
ConfigurableExtensionDataRef<
{
logoIcon?: JSX.Element | undefined;
logoFull?: JSX.Element | undefined;
},
'core.nav-logo.logo-elements',
{}
>,
{
singleton: true;
optional: true;
}
>;
};
kind: undefined;
namespace: 'app';
name: 'nav';
}>;
'app/root': ExtensionDefinition<{
config: {};
configInput: {};
output: ConfigurableExtensionDataRef<
JSX_2.Element,
'core.reactElement',
{}
>;
inputs: {
router: ExtensionInput<
ConfigurableExtensionDataRef<
ComponentType<{
children?: ReactNode;
}>,
'app.router.wrapper',
{}
>,
{
singleton: true;
optional: true;
}
>;
signInPage: ExtensionInput<
ConfigurableExtensionDataRef<
ComponentType<SignInPageProps>,
'core.sign-in-page.component',
{}
>,
{
singleton: true;
optional: true;
}
>;
children: ExtensionInput<
ConfigurableExtensionDataRef<JSX_2.Element, 'core.reactElement', {}>,
{
singleton: true;
optional: false;
}
>;
elements: ExtensionInput<
ConfigurableExtensionDataRef<JSX_2.Element, 'core.reactElement', {}>,
{
singleton: false;
optional: false;
}
>;
wrappers: ExtensionInput<
ConfigurableExtensionDataRef<
ComponentType<{
children?: ReactNode;
}>,
'app.root.wrapper',
{}
>,
{
singleton: false;
optional: false;
}
>;
};
kind: undefined;
namespace: 'app';
name: 'root';
}>;
'app/routes': ExtensionDefinition<{
config: {};
configInput: {};
output: ConfigurableExtensionDataRef<
JSX_2.Element,
'core.reactElement',
{}
>;
inputs: {
routes: ExtensionInput<
| ConfigurableExtensionDataRef<JSX_2.Element, 'core.reactElement', {}>
| ConfigurableExtensionDataRef<string, 'core.routing.path', {}>
| ConfigurableExtensionDataRef<
RouteRef<AnyRouteRefParams>,
'core.routing.ref',
{
optional: true;
}
>,
{
singleton: false;
optional: false;
}
>;
};
kind: undefined;
namespace: 'app';
name: 'routes';
}>;
'api:app/app-theme': ExtensionDefinition<{
config: {};
configInput: {};
output: ConfigurableExtensionDataRef<
AnyApiFactory,
'core.api.factory',
{}
>;
inputs: {
themes: ExtensionInput<
ConfigurableExtensionDataRef<AppTheme, 'core.theme.theme', {}>,
{
singleton: false;
optional: false;
}
>;
};
kind: 'api';
namespace: undefined;
name: 'app-theme';
}>;
'theme:app/light': ExtensionDefinition<{
kind: 'theme';
namespace: 'app';
name: 'light';
config: {};
configInput: {};
output: ConfigurableExtensionDataRef<AppTheme, 'core.theme.theme', {}>;
inputs: {};
}>;
'theme:app/dark': ExtensionDefinition<{
kind: 'theme';
namespace: 'app';
name: 'dark';
config: {};
configInput: {};
output: ConfigurableExtensionDataRef<AppTheme, 'core.theme.theme', {}>;
inputs: {};
}>;
'api:app/components': ExtensionDefinition<{
config: {};
configInput: {};
output: ConfigurableExtensionDataRef<
AnyApiFactory,
'core.api.factory',
{}
>;
inputs: {
components: ExtensionInput<
ConfigurableExtensionDataRef<
{
ref: ComponentRef;
impl: ComponentType;
},
'core.component.component',
{}
>,
{
singleton: false;
optional: false;
}
>;
};
kind: 'api';
namespace: undefined;
name: 'components';
}>;
'api:app/icons': ExtensionDefinition<{
config: {};
configInput: {};
output: ConfigurableExtensionDataRef<
AnyApiFactory,
'core.api.factory',
{}
>;
inputs: {
icons: ExtensionInput<
ConfigurableExtensionDataRef<
{
[x: string]: IconComponent_2;
},
'core.icons',
{}
>,
{
singleton: false;
optional: false;
}
>;
};
kind: 'api';
namespace: undefined;
name: 'icons';
}>;
'api:app/feature-flags': ExtensionDefinition<{
kind: 'api';
namespace: undefined;
name: 'feature-flags';
config: {};
configInput: {};
output: ConfigurableExtensionDataRef<
AnyApiFactory,
'core.api.factory',
{}
>;
inputs: {};
}>;
'api:app/translations': ExtensionDefinition<{
config: {};
configInput: {};
output: ConfigurableExtensionDataRef<
AnyApiFactory,
'core.api.factory',
{}
>;
inputs: {
translations: ExtensionInput<
ConfigurableExtensionDataRef<
| TranslationResource<string>
| TranslationMessages<
string,
{
[x: string]: string;
},
boolean
>,
'core.translation.translation',
{}
>,
{
singleton: false;
optional: false;
}
>;
};
kind: 'api';
namespace: undefined;
name: 'translations';
}>;
'app-root-element:app/oauth-request-dialog': ExtensionDefinition<{
kind: 'app-root-element';
namespace: 'app';
name: 'oauth-request-dialog';
config: {};
configInput: {};
output: ConfigurableExtensionDataRef<
JSX_2.Element,
'core.reactElement',
{}
>;
inputs: {};
}>;
'app-root-element:app/alert-display': ExtensionDefinition<{
config: {
transientTimeoutMs: number;
anchorOrigin: {
horizontal: 'center' | 'left' | 'right';
vertical: 'top' | 'bottom';
};
};
configInput: {
anchorOrigin?:
| {
horizontal?: 'center' | 'left' | 'right' | undefined;
vertical?: 'top' | 'bottom' | undefined;
}
| undefined;
transientTimeoutMs?: number | undefined;
};
output: ConfigurableExtensionDataRef<
JSX_2.Element,
'core.reactElement',
{}
>;
inputs: {
[x: string]: ExtensionInput<
AnyExtensionDataRef,
{
optional: boolean;
singleton: boolean;
}
>;
};
kind: 'app-root-element';
namespace: 'app';
name: 'alert-display';
}>;
'api:app/discovery': ExtensionDefinition<{
kind: 'api';
namespace: undefined;
name: 'discovery';
config: {};
configInput: {};
output: ConfigurableExtensionDataRef<
AnyApiFactory,
'core.api.factory',
{}
>;
inputs: {};
}>;
'api:app/alert': ExtensionDefinition<{
kind: 'api';
namespace: undefined;
name: 'alert';
config: {};
configInput: {};
output: ConfigurableExtensionDataRef<
AnyApiFactory,
'core.api.factory',
{}
>;
inputs: {};
}>;
'api:app/analytics': ExtensionDefinition<{
kind: 'api';
namespace: undefined;
name: 'analytics';
config: {};
configInput: {};
output: ConfigurableExtensionDataRef<
AnyApiFactory,
'core.api.factory',
{}
>;
inputs: {};
}>;
'api:app/error': ExtensionDefinition<{
kind: 'api';
namespace: undefined;
name: 'error';
config: {};
configInput: {};
output: ConfigurableExtensionDataRef<
AnyApiFactory,
'core.api.factory',
{}
>;
inputs: {};
}>;
'api:app/storage': ExtensionDefinition<{
kind: 'api';
namespace: undefined;
name: 'storage';
config: {};
configInput: {};
output: ConfigurableExtensionDataRef<
AnyApiFactory,
'core.api.factory',
{}
>;
inputs: {};
}>;
'api:app/fetch': ExtensionDefinition<{
kind: 'api';
namespace: undefined;
name: 'fetch';
config: {};
configInput: {};
output: ConfigurableExtensionDataRef<
AnyApiFactory,
'core.api.factory',
{}
>;
inputs: {};
}>;
'api:app/oauth-request': ExtensionDefinition<{
kind: 'api';
namespace: undefined;
name: 'oauth-request';
config: {};
configInput: {};
output: ConfigurableExtensionDataRef<
AnyApiFactory,
'core.api.factory',
{}
>;
inputs: {};
}>;
'api:app/google-auth': ExtensionDefinition<{
kind: 'api';
namespace: undefined;
name: 'google-auth';
config: {};
configInput: {};
output: ConfigurableExtensionDataRef<
AnyApiFactory,
'core.api.factory',
{}
>;
inputs: {};
}>;
'api:app/microsoft-auth': ExtensionDefinition<{
kind: 'api';
namespace: undefined;
name: 'microsoft-auth';
config: {};
configInput: {};
output: ConfigurableExtensionDataRef<
AnyApiFactory,
'core.api.factory',
{}
>;
inputs: {};
}>;
'api:app/github-auth': ExtensionDefinition<{
kind: 'api';
namespace: undefined;
name: 'github-auth';
config: {};
configInput: {};
output: ConfigurableExtensionDataRef<
AnyApiFactory,
'core.api.factory',
{}
>;
inputs: {};
}>;
'api:app/okta-auth': ExtensionDefinition<{
kind: 'api';
namespace: undefined;
name: 'okta-auth';
config: {};
configInput: {};
output: ConfigurableExtensionDataRef<
AnyApiFactory,
'core.api.factory',
{}
>;
inputs: {};
}>;
'api:app/gitlab-auth': ExtensionDefinition<{
kind: 'api';
namespace: undefined;
name: 'gitlab-auth';
config: {};
configInput: {};
output: ConfigurableExtensionDataRef<
AnyApiFactory,
'core.api.factory',
{}
>;
inputs: {};
}>;
'api:app/onelogin-auth': ExtensionDefinition<{
kind: 'api';
namespace: undefined;
name: 'onelogin-auth';
config: {};
configInput: {};
output: ConfigurableExtensionDataRef<
AnyApiFactory,
'core.api.factory',
{}
>;
inputs: {};
}>;
'api:app/bitbucket-auth': ExtensionDefinition<{
kind: 'api';
namespace: undefined;
name: 'bitbucket-auth';
config: {};
configInput: {};
output: ConfigurableExtensionDataRef<
AnyApiFactory,
'core.api.factory',
{}
>;
inputs: {};
}>;
'api:app/bitbucket-server-auth': ExtensionDefinition<{
kind: 'api';
namespace: undefined;
name: 'bitbucket-server-auth';
config: {};
configInput: {};
output: ConfigurableExtensionDataRef<
AnyApiFactory,
'core.api.factory',
{}
>;
inputs: {};
}>;
'api:app/atlassian-auth': ExtensionDefinition<{
kind: 'api';
namespace: undefined;
name: 'atlassian-auth';
config: {};
configInput: {};
output: ConfigurableExtensionDataRef<
AnyApiFactory,
'core.api.factory',
{}
>;
inputs: {};
}>;
'api:app/vmware-cloud-auth': ExtensionDefinition<{
kind: 'api';
namespace: undefined;
name: 'vmware-cloud-auth';
config: {};
configInput: {};
output: ConfigurableExtensionDataRef<
AnyApiFactory,
'core.api.factory',
{}
>;
inputs: {};
}>;
'api:app/permission': ExtensionDefinition<{
kind: 'api';
namespace: undefined;
name: 'permission';
config: {};
configInput: {};
output: ConfigurableExtensionDataRef<
AnyApiFactory,
'core.api.factory',
{}
>;
inputs: {};
}>;
}
>;
export default appPlugin;
// (No @packageDocumentation comment for this package)
```
+9
View File
@@ -0,0 +1,9 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage-plugin-app
title: '@backstage/plugin-app'
spec:
lifecycle: experimental
type: backstage-frontend-plugin
owner: maintainers
+65
View File
@@ -0,0 +1,65 @@
{
"name": "@backstage/plugin-app",
"version": "0.0.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
"types": "dist/index.d.ts"
},
"repository": {
"type": "git",
"url": "https://github.com/backstage/backstage",
"directory": "plugins/app"
},
"backstage": {
"role": "frontend-plugin",
"pluginId": "app",
"pluginPackages": [
"@backstage/plugin-app",
"@backstage/plugin-app-backend",
"@backstage/plugin-app-node"
]
},
"sideEffects": false,
"scripts": {
"start": "backstage-cli package start",
"build": "backstage-cli package build",
"lint": "backstage-cli package lint",
"test": "backstage-cli package test",
"clean": "backstage-cli package clean",
"prepack": "backstage-cli package prepack",
"postpack": "backstage-cli package postpack"
},
"dependencies": {
"@backstage/core-components": "workspace:^",
"@backstage/core-plugin-api": "workspace:^",
"@backstage/frontend-plugin-api": "workspace:^",
"@backstage/plugin-permission-react": "workspace:^",
"@backstage/theme": "workspace:^",
"@material-ui/core": "^4.9.13",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "^4.0.0-alpha.61",
"@types/react": "^16.13.1 || ^17.0.0 || ^18.0.0",
"react-use": "^17.2.4"
},
"peerDependencies": {
"react": "^16.13.1 || ^17.0.0 || ^18.0.0",
"react-router-dom": "6.0.0-beta.0 || ^6.3.0"
},
"devDependencies": {
"@backstage/cli": "workspace:^",
"@backstage/dev-utils": "workspace:^",
"@backstage/frontend-test-utils": "workspace:^",
"@testing-library/jest-dom": "^6.0.0",
"@testing-library/react": "^15.0.0",
"@testing-library/user-event": "^14.0.0",
"msw": "^1.0.0",
"react": "^16.13.1 || ^17.0.0 || ^18.0.0"
},
"files": [
"dist"
]
}
+382
View File
@@ -0,0 +1,382 @@
/*
* 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.
*/
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import {
AlertApiForwarder,
NoOpAnalyticsApi,
ErrorApiForwarder,
ErrorAlerter,
GoogleAuth,
GithubAuth,
OktaAuth,
GitlabAuth,
MicrosoftAuth,
BitbucketAuth,
BitbucketServerAuth,
OAuthRequestManager,
WebStorage,
UrlPatternDiscovery,
OneLoginAuth,
UnhandledErrorForwarder,
AtlassianAuth,
createFetchApi,
FetchMiddlewares,
VMwareCloudAuth,
} from '../../../packages/core-app-api/src/apis/implementations';
import {
createApiFactory,
alertApiRef,
analyticsApiRef,
errorApiRef,
discoveryApiRef,
fetchApiRef,
identityApiRef,
oauthRequestApiRef,
googleAuthApiRef,
githubAuthApiRef,
oktaAuthApiRef,
gitlabAuthApiRef,
microsoftAuthApiRef,
storageApiRef,
configApiRef,
oneloginAuthApiRef,
bitbucketAuthApiRef,
bitbucketServerAuthApiRef,
atlassianAuthApiRef,
vmwareCloudAuthApiRef,
} from '@backstage/core-plugin-api';
import { ApiBlueprint } from '@backstage/frontend-plugin-api';
import {
permissionApiRef,
IdentityPermissionApi,
} from '@backstage/plugin-permission-react';
export const apis = [
ApiBlueprint.make({
name: 'discovery',
params: {
factory: createApiFactory({
api: discoveryApiRef,
deps: { configApi: configApiRef },
factory: ({ configApi }) =>
UrlPatternDiscovery.compile(
`${configApi.getString('backend.baseUrl')}/api/{{ pluginId }}`,
),
}),
},
}),
ApiBlueprint.make({
name: 'alert',
params: {
factory: createApiFactory({
api: alertApiRef,
deps: {},
factory: () => new AlertApiForwarder(),
}),
},
}),
ApiBlueprint.make({
name: 'analytics',
params: {
factory: createApiFactory({
api: analyticsApiRef,
deps: {},
factory: () => new NoOpAnalyticsApi(),
}),
},
}),
ApiBlueprint.make({
name: 'error',
params: {
factory: createApiFactory({
api: errorApiRef,
deps: { alertApi: alertApiRef },
factory: ({ alertApi }) => {
const errorApi = new ErrorAlerter(alertApi, new ErrorApiForwarder());
UnhandledErrorForwarder.forward(errorApi, { hidden: false });
return errorApi;
},
}),
},
}),
ApiBlueprint.make({
name: 'storage',
params: {
factory: createApiFactory({
api: storageApiRef,
deps: { errorApi: errorApiRef },
factory: ({ errorApi }) => WebStorage.create({ errorApi }),
}),
},
}),
ApiBlueprint.make({
name: 'fetch',
params: {
factory: createApiFactory({
api: fetchApiRef,
deps: {
configApi: configApiRef,
identityApi: identityApiRef,
discoveryApi: discoveryApiRef,
},
factory: ({ configApi, identityApi, discoveryApi }) => {
return createFetchApi({
middleware: [
FetchMiddlewares.resolvePluginProtocol({
discoveryApi,
}),
FetchMiddlewares.injectIdentityAuth({
identityApi,
config: configApi,
}),
],
});
},
}),
},
}),
ApiBlueprint.make({
name: 'oauth-request',
params: {
factory: createApiFactory({
api: oauthRequestApiRef,
deps: {},
factory: () => new OAuthRequestManager(),
}),
},
}),
ApiBlueprint.make({
name: 'google-auth',
params: {
factory: createApiFactory({
api: googleAuthApiRef,
deps: {
discoveryApi: discoveryApiRef,
oauthRequestApi: oauthRequestApiRef,
configApi: configApiRef,
},
factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
GoogleAuth.create({
configApi,
discoveryApi,
oauthRequestApi,
environment: configApi.getOptionalString('auth.environment'),
}),
}),
},
}),
ApiBlueprint.make({
name: 'microsoft-auth',
params: {
factory: createApiFactory({
api: microsoftAuthApiRef,
deps: {
discoveryApi: discoveryApiRef,
oauthRequestApi: oauthRequestApiRef,
configApi: configApiRef,
},
factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
MicrosoftAuth.create({
configApi,
discoveryApi,
oauthRequestApi,
environment: configApi.getOptionalString('auth.environment'),
}),
}),
},
}),
ApiBlueprint.make({
name: 'github-auth',
params: {
factory: createApiFactory({
api: githubAuthApiRef,
deps: {
discoveryApi: discoveryApiRef,
oauthRequestApi: oauthRequestApiRef,
configApi: configApiRef,
},
factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
GithubAuth.create({
configApi,
discoveryApi,
oauthRequestApi,
defaultScopes: ['read:user'],
environment: configApi.getOptionalString('auth.environment'),
}),
}),
},
}),
ApiBlueprint.make({
name: 'okta-auth',
params: {
factory: createApiFactory({
api: oktaAuthApiRef,
deps: {
discoveryApi: discoveryApiRef,
oauthRequestApi: oauthRequestApiRef,
configApi: configApiRef,
},
factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
OktaAuth.create({
configApi,
discoveryApi,
oauthRequestApi,
environment: configApi.getOptionalString('auth.environment'),
}),
}),
},
}),
ApiBlueprint.make({
name: 'gitlab-auth',
params: {
factory: createApiFactory({
api: gitlabAuthApiRef,
deps: {
discoveryApi: discoveryApiRef,
oauthRequestApi: oauthRequestApiRef,
configApi: configApiRef,
},
factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
GitlabAuth.create({
configApi,
discoveryApi,
oauthRequestApi,
environment: configApi.getOptionalString('auth.environment'),
}),
}),
},
}),
ApiBlueprint.make({
name: 'onelogin-auth',
params: {
factory: createApiFactory({
api: oneloginAuthApiRef,
deps: {
discoveryApi: discoveryApiRef,
oauthRequestApi: oauthRequestApiRef,
configApi: configApiRef,
},
factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
OneLoginAuth.create({
configApi,
discoveryApi,
oauthRequestApi,
environment: configApi.getOptionalString('auth.environment'),
}),
}),
},
}),
ApiBlueprint.make({
name: 'bitbucket-auth',
params: {
factory: createApiFactory({
api: bitbucketAuthApiRef,
deps: {
discoveryApi: discoveryApiRef,
oauthRequestApi: oauthRequestApiRef,
configApi: configApiRef,
},
factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
BitbucketAuth.create({
configApi,
discoveryApi,
oauthRequestApi,
defaultScopes: ['account'],
environment: configApi.getOptionalString('auth.environment'),
}),
}),
},
}),
ApiBlueprint.make({
name: 'bitbucket-server-auth',
params: {
factory: createApiFactory({
api: bitbucketServerAuthApiRef,
deps: {
discoveryApi: discoveryApiRef,
oauthRequestApi: oauthRequestApiRef,
configApi: configApiRef,
},
factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
BitbucketServerAuth.create({
configApi,
discoveryApi,
oauthRequestApi,
defaultScopes: ['REPO_READ'],
}),
}),
},
}),
ApiBlueprint.make({
name: 'atlassian-auth',
params: {
factory: createApiFactory({
api: atlassianAuthApiRef,
deps: {
discoveryApi: discoveryApiRef,
oauthRequestApi: oauthRequestApiRef,
configApi: configApiRef,
},
factory: ({ discoveryApi, oauthRequestApi, configApi }) => {
return AtlassianAuth.create({
configApi,
discoveryApi,
oauthRequestApi,
environment: configApi.getOptionalString('auth.environment'),
});
},
}),
},
}),
ApiBlueprint.make({
name: 'vmware-cloud-auth',
params: {
factory: createApiFactory({
api: vmwareCloudAuthApiRef,
deps: {
discoveryApi: discoveryApiRef,
oauthRequestApi: oauthRequestApiRef,
configApi: configApiRef,
},
factory: ({ discoveryApi, oauthRequestApi, configApi }) => {
return VMwareCloudAuth.create({
configApi,
discoveryApi,
oauthRequestApi,
environment: configApi.getOptionalString('auth.environment'),
});
},
}),
},
}),
ApiBlueprint.make({
name: 'permission',
params: {
factory: createApiFactory({
api: permissionApiRef,
deps: {
discovery: discoveryApiRef,
identity: identityApiRef,
config: configApiRef,
},
factory: ({ config, discovery, identity }) =>
IdentityPermissionApi.create({ config, discovery, identity }),
}),
},
}),
] as const;
@@ -21,6 +21,10 @@ import {
createExtension,
createExtensionInput,
} from '@backstage/frontend-plugin-api';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { ApiProvider } from '../../../../packages/core-app-api/src';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { AppThemeProvider } from '../../../../packages/core-app-api/src/app/AppThemeProvider';
export const App = createExtension({
namespace: 'app',
@@ -31,11 +35,17 @@ export const App = createExtension({
}),
},
output: [coreExtensionData.reactElement],
factory: ({ node, inputs }) => [
coreExtensionData.reactElement(
<ExtensionBoundary node={node}>
{inputs.root.get(coreExtensionData.reactElement)}
</ExtensionBoundary>,
),
],
factory: ({ node, apis, inputs }) => {
return [
coreExtensionData.reactElement(
<ApiProvider apis={apis}>
<AppThemeProvider>
<ExtensionBoundary node={node}>
{inputs.root.get(coreExtensionData.reactElement)}
</ExtensionBoundary>
</AppThemeProvider>
</ApiProvider>,
),
];
},
});
@@ -15,7 +15,7 @@
*/
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { AppLanguageSelector } from '../../../core-app-api/src/apis/implementations/AppLanguageApi';
import { AppLanguageSelector } from '../../../../packages/core-app-api/src/apis/implementations/AppLanguageApi';
import { appLanguageApiRef } from '@backstage/core-plugin-api/alpha';
import { ApiBlueprint, createApiFactory } from '@backstage/frontend-plugin-api';
@@ -33,9 +33,9 @@ import {
SidebarItem,
} from '@backstage/core-components';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import LogoIcon from '../../../app/src/components/Root/LogoIcon';
import LogoIcon from '../../../../packages/app/src/components/Root/LogoIcon';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import LogoFull from '../../../app/src/components/Root/LogoFull';
import LogoFull from '../../../../packages/app/src/components/Root/LogoFull';
const useSidebarLogoStyles = makeStyles({
root: {
@@ -19,7 +19,6 @@ import React, {
Fragment,
PropsWithChildren,
ReactNode,
useContext,
useState,
} from 'react';
import {
@@ -27,21 +26,31 @@ import {
RouterBlueprint,
SignInPageBlueprint,
coreExtensionData,
discoveryApiRef,
fetchApiRef,
errorApiRef,
createExtension,
createExtensionInput,
routeResolutionApiRef,
} from '@backstage/frontend-plugin-api';
import {
DiscoveryApi,
ErrorApi,
FetchApi,
IdentityApi,
ProfileInfo,
SignInPageProps,
configApiRef,
identityApiRef,
useApi,
} from '@backstage/core-plugin-api';
import { InternalAppContext } from '../wiring/InternalAppContext';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { AppIdentityProxy } from '../../../core-app-api/src/apis/implementations/IdentityApi/AppIdentityProxy';
import { isProtectedApp } from '../../../../packages/core-app-api/src/app/isProtectedApp';
import { BrowserRouter } from 'react-router-dom';
import { RouteTracker } from '../routing/RouteTracker';
import { getBasePath } from '../routing/getBasePath';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { RouteTracker } from '../../../../packages/frontend-app-api/src/routing/RouteTracker';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { getBasePath } from '../../../../packages/frontend-app-api/src/routing/getBasePath';
export const AppRoot = createExtension({
namespace: 'app',
@@ -65,7 +74,28 @@ export const AppRoot = createExtension({
]),
},
output: [coreExtensionData.reactElement],
factory({ inputs }) {
factory({ inputs, apis }) {
if (isProtectedApp()) {
const identityApi = apis.get(identityApiRef);
if (!identityApi) {
throw new Error('App requires an Identity API implementation');
}
const appIdentityProxy = toAppIdentityProxy(identityApi);
const discoveryApi = apis.get(discoveryApiRef);
const errorApi = apis.get(errorApiRef);
const fetchApi = apis.get(fetchApiRef);
if (!discoveryApi || !errorApi || !fetchApi) {
throw new Error(
'App is running in protected mode but missing required APIs',
);
}
appIdentityProxy.enableCookieAuth({
discoveryApi,
errorApi,
fetchApi,
});
}
let content: React.ReactNode = (
<>
{inputs.elements.map(el => (
@@ -123,6 +153,33 @@ function SignInPageWrapper({
return <>{children}</>;
}
type AppIdentityProxy = IdentityApi & {
enableCookieAuth(ctx: {
errorApi: ErrorApi;
fetchApi: FetchApi;
discoveryApi: DiscoveryApi;
}): void;
setTarget(
impl: IdentityApi & /* backwards compat stuff */ {
getUserId?(): string;
getIdToken?(): Promise<string | undefined>;
getProfile?(): ProfileInfo;
},
options: { signOutTargetUrl: string },
): void;
};
function toAppIdentityProxy(identityApi: IdentityApi): AppIdentityProxy {
if (!('enableCookieAuth' in identityApi)) {
throw new Error('Unexpected Identity API implementation');
}
return identityApi as AppIdentityProxy;
}
type RouteResolverProxy = {
getRouteObjects(): any[];
};
/**
* Props for the {@link AppRouter} component.
* @public
@@ -157,12 +214,17 @@ export function AppRouter(props: AppRouterProps) {
} = props;
const configApi = useApi(configApiRef);
const appIdentityProxy = toAppIdentityProxy(useApi(identityApiRef));
const routeResolutionsApi = useApi(routeResolutionApiRef);
const basePath = getBasePath(configApi);
const internalAppContext = useContext(InternalAppContext);
if (!internalAppContext) {
throw new Error('AppRouter must be rendered within the AppProvider');
// TODO: Private access for now, probably replace with path -> node lookup method on the API
if (!('getRouteObjects' in routeResolutionsApi)) {
throw new Error('Unexpected route resolution API implementation');
}
const { routeObjects, appIdentityProxy } = internalAppContext;
const routeObjects = (
routeResolutionsApi as RouteResolverProxy
).getRouteObjects();
// If the app hasn't configured a sign-in page, we just continue as guest.
if (!SignInPageComponent) {
@@ -28,7 +28,8 @@ import {
createApiFactory,
appThemeApiRef,
} from '@backstage/frontend-plugin-api';
import { AppThemeSelector } from '@backstage/core-app-api';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { AppThemeSelector } from '../../../../packages/core-app-api/src/apis/implementations';
/**
* Contains the themes installed into the app.
@@ -21,7 +21,8 @@ import {
createApiFactory,
componentsApiRef,
} from '@backstage/frontend-plugin-api';
import { DefaultComponentsApi } from '../apis/implementations/ComponentsApi';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { DefaultComponentsApi } from '../../../../packages/frontend-app-api/src/apis/implementations/ComponentsApi';
/**
* Contains the shareable components installed into the app.
@@ -20,7 +20,7 @@ import {
featureFlagsApiRef,
} from '@backstage/frontend-plugin-api';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { LocalStorageFeatureFlags } from '../../../core-app-api/src/apis/implementations/FeatureFlagsApi/LocalStorageFeatureFlags';
import { LocalStorageFeatureFlags } from '../../../../packages/core-app-api/src/apis/implementations/FeatureFlagsApi/LocalStorageFeatureFlags';
/**
* Contains the shareable icons installed into the app.
@@ -21,9 +21,10 @@ import {
createApiFactory,
iconsApiRef,
} from '@backstage/frontend-plugin-api';
import { DefaultIconsApi } from '../apis/implementations/IconsApi';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { icons as defaultIcons } from '../../../app-defaults/src/defaults';
import { DefaultIconsApi } from '../../../../packages/frontend-app-api/src/apis/implementations/IconsApi';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { icons as defaultIcons } from '../../../../packages/app-defaults/src/defaults';
/**
* Contains the shareable icons installed into the app.
@@ -25,7 +25,7 @@ import {
} from '@backstage/core-plugin-api/alpha';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { I18nextTranslationApi } from '../../../core-app-api/src/apis/implementations/TranslationApi/I18nextTranslationApi';
import { I18nextTranslationApi } from '../../../../packages/core-app-api/src/apis/implementations/TranslationApi/I18nextTranslationApi';
/**
* Contains translations that are installed in the app.
@@ -24,8 +24,7 @@ import {
} from '@backstage/frontend-plugin-api';
import { ErrorPanel } from '@backstage/core-components';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { components as defaultComponents } from '../../../app-defaults/src/defaults';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { components as defaultComponents } from '../../../../packages/app-defaults/src/defaults';
export const DefaultProgressComponent = createComponentExtension({
ref: coreComponentRefs.progress,
+35
View File
@@ -0,0 +1,35 @@
/*
* Copyright 2024 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 { App } from './App';
export { AppLanguageApi } from './AppLanguageApi';
export { AppLayout } from './AppLayout';
export { AppNav } from './AppNav';
export { AppRoot } from './AppRoot';
export { AppRoutes } from './AppRoutes';
export { AppThemeApi, DarkTheme, LightTheme } from './AppThemeApi';
export { ComponentsApi } from './ComponentsApi';
export { IconsApi } from './IconsApi';
export { FeatureFlagsApi } from './FeatureFlagsApi';
export { TranslationsApi } from './TranslationsApi';
export {
DefaultProgressComponent,
DefaultErrorBoundaryComponent,
DefaultNotFoundErrorPageComponent,
} from './components';
export {
oauthRequestDialogAppRootElement,
alertDisplayAppRootElement,
} from './elements';
+17
View File
@@ -0,0 +1,17 @@
/*
* Copyright 2024 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 { appPlugin as default } from './plugin';
+22
View File
@@ -0,0 +1,22 @@
/*
* Copyright 2024 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 { appPlugin } from './plugin';
describe('app', () => {
it('should export plugin', () => {
expect(appPlugin).toBeDefined();
});
});
+64
View File
@@ -0,0 +1,64 @@
/*
* Copyright 2024 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 { createFrontendPlugin } from '@backstage/frontend-plugin-api';
import {
App,
AppLanguageApi,
AppLayout,
AppNav,
AppRoot,
AppRoutes,
AppThemeApi,
DarkTheme,
LightTheme,
ComponentsApi,
IconsApi,
FeatureFlagsApi,
TranslationsApi,
DefaultProgressComponent,
DefaultNotFoundErrorPageComponent,
DefaultErrorBoundaryComponent,
oauthRequestDialogAppRootElement,
alertDisplayAppRootElement,
} from './extensions';
import { apis } from './defaultApis';
/** @public */
export const appPlugin = createFrontendPlugin({
id: 'app',
extensions: [
...apis,
App,
AppLanguageApi,
AppLayout,
AppNav,
AppRoot,
AppRoutes,
AppThemeApi,
DarkTheme,
LightTheme,
ComponentsApi,
IconsApi,
FeatureFlagsApi,
TranslationsApi,
DefaultProgressComponent,
DefaultNotFoundErrorPageComponent,
DefaultErrorBoundaryComponent,
oauthRequestDialogAppRootElement,
alertDisplayAppRootElement,
],
});
+16
View File
@@ -0,0 +1,16 @@
/*
* Copyright 2024 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 '@testing-library/jest-dom';
+2
View File
@@ -60,6 +60,8 @@ const roleRules = [
targetRole: 'frontend-plugin',
except: [
// TODO(freben): Address these
'@backstage/frontend-app-api',
'@backstage/frontend-test-utils',
'@backstage/plugin-api-docs',
'@backstage/plugin-techdocs-addons-test-utils',
],
+43 -12
View File
@@ -4438,6 +4438,7 @@ __metadata:
"@backstage/core-plugin-api": "workspace:^"
"@backstage/errors": "workspace:^"
"@backstage/frontend-plugin-api": "workspace:^"
"@backstage/plugin-app": "workspace:^"
"@backstage/test-utils": "workspace:^"
"@backstage/theme": "workspace:^"
"@backstage/types": "workspace:^"
@@ -4488,6 +4489,7 @@ __metadata:
"@backstage/config": "workspace:^"
"@backstage/frontend-app-api": "workspace:^"
"@backstage/frontend-plugin-api": "workspace:^"
"@backstage/plugin-app": "workspace:^"
"@backstage/test-utils": "workspace:^"
"@backstage/types": "workspace:^"
"@testing-library/jest-dom": ^6.0.0
@@ -4682,6 +4684,34 @@ __metadata:
languageName: unknown
linkType: soft
"@backstage/plugin-app@workspace:^, @backstage/plugin-app@workspace:plugins/app":
version: 0.0.0-use.local
resolution: "@backstage/plugin-app@workspace:plugins/app"
dependencies:
"@backstage/cli": "workspace:^"
"@backstage/core-components": "workspace:^"
"@backstage/core-plugin-api": "workspace:^"
"@backstage/dev-utils": "workspace:^"
"@backstage/frontend-plugin-api": "workspace:^"
"@backstage/frontend-test-utils": "workspace:^"
"@backstage/plugin-permission-react": "workspace:^"
"@backstage/theme": "workspace:^"
"@material-ui/core": ^4.9.13
"@material-ui/icons": ^4.9.1
"@material-ui/lab": ^4.0.0-alpha.61
"@testing-library/jest-dom": ^6.0.0
"@testing-library/react": ^15.0.0
"@testing-library/user-event": ^14.0.0
"@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0
msw: ^1.0.0
react: ^16.13.1 || ^17.0.0 || ^18.0.0
react-use: ^17.2.4
peerDependencies:
react: ^16.13.1 || ^17.0.0 || ^18.0.0
react-router-dom: 6.0.0-beta.0 || ^6.3.0
languageName: unknown
linkType: soft
"@backstage/plugin-auth-backend-module-atlassian-provider@workspace:^, @backstage/plugin-auth-backend-module-atlassian-provider@workspace:plugins/auth-backend-module-atlassian-provider":
version: 0.0.0-use.local
resolution: "@backstage/plugin-auth-backend-module-atlassian-provider@workspace:plugins/auth-backend-module-atlassian-provider"
@@ -26731,6 +26761,7 @@ __metadata:
"@backstage/frontend-plugin-api": "workspace:^"
"@backstage/integration-react": "workspace:^"
"@backstage/plugin-api-docs": "workspace:^"
"@backstage/plugin-app": "workspace:^"
"@backstage/plugin-app-visualizer": "workspace:^"
"@backstage/plugin-auth-react": "workspace:^"
"@backstage/plugin-catalog": "workspace:^"
@@ -30324,9 +30355,9 @@ __metadata:
linkType: hard
"is-map@npm:^2.0.1":
version: 2.0.2
resolution: "is-map@npm:2.0.2"
checksum: ace3d0ecd667bbdefdb1852de601268f67f2db725624b1958f279316e13fecb8fa7df91fd60f690d7417b4ec180712f5a7ee967008e27c65cfd475cc84337728
version: 2.0.3
resolution: "is-map@npm:2.0.3"
checksum: e6ce5f6380f32b141b3153e6ba9074892bbbbd655e92e7ba5ff195239777e767a976dcd4e22f864accaf30e53ebf961ab1995424aef91af68788f0591b7396cc
languageName: node
linkType: hard
@@ -30541,9 +30572,9 @@ __metadata:
linkType: hard
"is-set@npm:^2.0.1":
version: 2.0.2
resolution: "is-set@npm:2.0.2"
checksum: b64343faf45e9387b97a6fd32be632ee7b269bd8183701f3b3f5b71a7cf00d04450ed8669d0bd08753e08b968beda96fca73a10fd0ff56a32603f64deba55a57
version: 2.0.3
resolution: "is-set@npm:2.0.3"
checksum: 36e3f8c44bdbe9496c9689762cc4110f6a6a12b767c5d74c0398176aa2678d4467e3bf07595556f2dba897751bde1422480212b97d973c7b08a343100b0c0dfe
languageName: node
linkType: hard
@@ -35645,12 +35676,12 @@ __metadata:
linkType: hard
"object-is@npm:^1.0.1":
version: 1.1.5
resolution: "object-is@npm:1.1.5"
version: 1.1.6
resolution: "object-is@npm:1.1.6"
dependencies:
call-bind: ^1.0.2
define-properties: ^1.1.3
checksum: 989b18c4cba258a6b74dc1d74a41805c1a1425bce29f6cabb50dcb1a6a651ea9104a1b07046739a49a5bb1bc49727bcb00efd5c55f932f6ea04ec8927a7901fe
call-bind: ^1.0.7
define-properties: ^1.2.1
checksum: 3ea22759967e6f2380a2cbbd0f737b42dc9ddb2dfefdb159a1b927fea57335e1b058b564bfa94417db8ad58cddab33621a035de6f5e5ad56d89f2dd03e66c6a1
languageName: node
linkType: hard
@@ -39044,7 +39075,7 @@ __metadata:
languageName: node
linkType: hard
"react@npm:^18.0.2":
"react@npm:^16.13.1 || ^17.0.0 || ^18.0.0, react@npm:^18.0.2":
version: 18.3.1
resolution: "react@npm:18.3.1"
dependencies: