chore: move things around a little bit to the app plugin

Co-authored-by: Patrik Oldsberg <poldsberg@gmail.com>
Co-authored-by: Fredrik Adelöw <freben@users.noreply.github.com>
Co-authored-by: Johan Haals <johan@haals.se>
Co-authored-by: Camila Belo <camilaibs@users.noreply.github.com>
Co-authored-by: Camila Belo <camilaibs@gmail.com>
Signed-off-by: blam <ben@blam.sh>
This commit is contained in:
blam
2024-08-26 13:32:06 +02:00
parent 5a97d446f6
commit 06448161aa
16 changed files with 40 additions and 28 deletions
+41
View File
@@ -0,0 +1,41 @@
/*
* Copyright 2023 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 {
ExtensionBoundary,
coreExtensionData,
createExtension,
createExtensionInput,
} from '@backstage/frontend-plugin-api';
export const App = createExtension({
namespace: 'app',
attachTo: { id: 'root', input: 'app' },
inputs: {
root: createExtensionInput([coreExtensionData.reactElement], {
singleton: true,
}),
},
output: [coreExtensionData.reactElement],
factory: ({ node, inputs }) => [
coreExtensionData.reactElement(
<ExtensionBoundary node={node}>
{inputs.root.get(coreExtensionData.reactElement)}
</ExtensionBoundary>,
),
],
});
@@ -0,0 +1,30 @@
/*
* 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.
*/
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
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';
export const AppLanguageApi = ApiBlueprint.make({
name: 'app-language',
params: {
factory: createApiFactory(
appLanguageApiRef,
AppLanguageSelector.createWithStorage(),
),
},
});
+46
View File
@@ -0,0 +1,46 @@
/*
* Copyright 2023 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 {
createExtension,
coreExtensionData,
createExtensionInput,
} from '@backstage/frontend-plugin-api';
import { SidebarPage } from '@backstage/core-components';
export const AppLayout = createExtension({
namespace: 'app',
name: 'layout',
attachTo: { id: 'app/root', input: 'children' },
inputs: {
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>,
),
],
});
+112
View File
@@ -0,0 +1,112 @@
/*
* Copyright 2023 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 {
createExtension,
coreExtensionData,
createExtensionInput,
useRouteRef,
NavItemBlueprint,
NavLogoBlueprint,
} from '@backstage/frontend-plugin-api';
import { makeStyles } from '@material-ui/core/styles';
import {
Sidebar,
useSidebarOpenState,
Link,
sidebarConfig,
SidebarDivider,
SidebarItem,
} from '@backstage/core-components';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import LogoIcon from '../../../../packages/app/src/components/Root/LogoIcon';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import LogoFull from '../../../../packages/app/src/components/Root/LogoFull';
const useSidebarLogoStyles = makeStyles({
root: {
width: sidebarConfig.drawerWidthClosed,
height: 3 * sidebarConfig.logoHeight,
display: 'flex',
flexFlow: 'row nowrap',
alignItems: 'center',
marginBottom: -14,
},
link: {
width: sidebarConfig.drawerWidthClosed,
marginLeft: 24,
},
});
const SidebarLogo = (
props: (typeof NavLogoBlueprint.dataRefs.logoElements)['T'],
) => {
const classes = useSidebarLogoStyles();
const { isOpen } = useSidebarOpenState();
return (
<div className={classes.root}>
<Link to="/" underline="none" className={classes.link} aria-label="Home">
{isOpen
? props?.logoFull ?? <LogoFull />
: props?.logoIcon ?? <LogoIcon />}
</Link>
</div>
);
};
const SidebarNavItem = (
props: (typeof NavItemBlueprint.dataRefs.target)['T'],
) => {
const { icon: Icon, title, routeRef } = props;
const link = useRouteRef(routeRef);
if (!link) {
return null;
}
// TODO: Support opening modal, for example, the search one
return <SidebarItem to={link()} icon={Icon} text={title} />;
};
export const AppNav = createExtension({
namespace: 'app',
name: 'nav',
attachTo: { id: 'app/layout', input: 'nav' },
inputs: {
items: createExtensionInput([NavItemBlueprint.dataRefs.target]),
logos: createExtensionInput([NavLogoBlueprint.dataRefs.logoElements], {
singleton: true,
optional: true,
}),
},
output: [coreExtensionData.reactElement],
factory: ({ inputs }) => [
coreExtensionData.reactElement(
<Sidebar>
<SidebarLogo
{...inputs.logos?.get(NavLogoBlueprint.dataRefs.logoElements)}
/>
<SidebarDivider />
{inputs.items.map((item, index) => (
<SidebarNavItem
{...item.get(NavItemBlueprint.dataRefs.target)}
key={index}
/>
))}
</Sidebar>,
),
],
});
+214
View File
@@ -0,0 +1,214 @@
/*
* Copyright 2023 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, {
ComponentType,
Fragment,
PropsWithChildren,
ReactNode,
useContext,
useState,
} from 'react';
import {
AppRootWrapperBlueprint,
RouterBlueprint,
SignInPageBlueprint,
coreExtensionData,
createExtension,
createExtensionInput,
} from '@backstage/frontend-plugin-api';
import {
IdentityApi,
SignInPageProps,
configApiRef,
useApi,
} from '@backstage/core-plugin-api';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { InternalAppContext } from '../../../../packages/frontend-app-api/src/wiring/InternalAppContext';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { AppIdentityProxy } from '../../../../packages/core-app-api/src/apis/implementations/IdentityApi/AppIdentityProxy';
import { BrowserRouter } from 'react-router-dom';
// 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',
name: 'root',
attachTo: { id: 'app', input: 'root' },
inputs: {
router: createExtensionInput([RouterBlueprint.dataRefs.component], {
singleton: true,
optional: true,
}),
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: [coreExtensionData.reactElement],
factory({ inputs }) {
let content: React.ReactNode = (
<>
{inputs.elements.map(el => (
<Fragment key={el.node.spec.id}>
{el.get(coreExtensionData.reactElement)}
</Fragment>
))}
{inputs.children.get(coreExtensionData.reactElement)}
</>
);
for (const wrapper of inputs.wrappers) {
const Component = wrapper.get(AppRootWrapperBlueprint.dataRefs.component);
content = <Component>{content}</Component>;
}
return [
coreExtensionData.reactElement(
<AppRouter
SignInPageComponent={inputs.signInPage?.get(
SignInPageBlueprint.dataRefs.component,
)}
RouterComponent={inputs.router?.get(
RouterBlueprint.dataRefs.component,
)}
>
{content}
</AppRouter>,
),
];
},
});
// This wraps the sign-in page and waits for sign-in to be completed before rendering the app
function SignInPageWrapper({
component: Component,
appIdentityProxy,
children,
}: {
component: ComponentType<SignInPageProps>;
appIdentityProxy: AppIdentityProxy;
children: ReactNode;
}) {
const [identityApi, setIdentityApi] = useState<IdentityApi>();
const configApi = useApi(configApiRef);
const basePath = getBasePath(configApi);
if (!identityApi) {
return <Component onSignInSuccess={setIdentityApi} />;
}
appIdentityProxy.setTarget(identityApi, {
signOutTargetUrl: basePath || '/',
});
return <>{children}</>;
}
/**
* Props for the {@link AppRouter} component.
* @public
*/
export interface AppRouterProps {
children?: ReactNode;
SignInPageComponent?: ComponentType<SignInPageProps>;
RouterComponent?: ComponentType<PropsWithChildren<{}>>;
}
function DefaultRouter(props: PropsWithChildren<{}>) {
const configApi = useApi(configApiRef);
const basePath = getBasePath(configApi);
return <BrowserRouter basename={basePath}>{props.children}</BrowserRouter>;
}
/**
* App router and sign-in page wrapper.
*
* @remarks
*
* The AppRouter provides the routing context and renders the sign-in page.
* Until the user has successfully signed in, this component will render
* the sign-in page. Once the user has signed-in, it will instead render
* the app, while providing routing and route tracking for the app.
*/
export function AppRouter(props: AppRouterProps) {
const {
children,
SignInPageComponent,
RouterComponent = DefaultRouter,
} = props;
const configApi = useApi(configApiRef);
const basePath = getBasePath(configApi);
const internalAppContext = useContext(InternalAppContext);
if (!internalAppContext) {
throw new Error('AppRouter must be rendered within the AppProvider');
}
const { routeObjects, appIdentityProxy } = internalAppContext;
// If the app hasn't configured a sign-in page, we just continue as guest.
if (!SignInPageComponent) {
appIdentityProxy.setTarget(
{
getUserId: () => 'guest',
getIdToken: async () => undefined,
getProfile: () => ({
email: 'guest@example.com',
displayName: 'Guest',
}),
getProfileInfo: async () => ({
email: 'guest@example.com',
displayName: 'Guest',
}),
getBackstageIdentity: async () => ({
type: 'user',
userEntityRef: 'user:default/guest',
ownershipEntityRefs: ['user:default/guest'],
}),
getCredentials: async () => ({}),
signOut: async () => {},
},
{ signOutTargetUrl: basePath || '/' },
);
return (
<RouterComponent>
<RouteTracker routeObjects={routeObjects} />
{children}
</RouterComponent>
);
}
return (
<RouterComponent>
<RouteTracker routeObjects={routeObjects} />
<SignInPageWrapper
component={SignInPageComponent}
appIdentityProxy={appIdentityProxy}
>
{children}
</SignInPageWrapper>
</RouterComponent>
);
}
+61
View File
@@ -0,0 +1,61 @@
/*
* Copyright 2023 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 {
createExtension,
coreExtensionData,
createExtensionInput,
coreComponentRefs,
useComponentRef,
} from '@backstage/frontend-plugin-api';
import { useRoutes } from 'react-router-dom';
export const AppRoutes = createExtension({
namespace: 'app',
name: 'routes',
attachTo: { id: 'app/layout', input: 'content' },
inputs: {
routes: createExtensionInput([
coreExtensionData.routePath,
coreExtensionData.routeRef.optional(),
coreExtensionData.reactElement,
]),
},
output: [coreExtensionData.reactElement],
factory({ inputs }) {
const Routes = () => {
const NotFoundErrorPage = useComponentRef(
coreComponentRefs.notFoundErrorPage,
);
const element = useRoutes([
...inputs.routes.map(route => ({
path: `${route.get(coreExtensionData.routePath)}/*`,
element: route.get(coreExtensionData.reactElement),
})),
{
path: '*',
element: <NotFoundErrorPage />,
},
]);
return element;
};
return [coreExtensionData.reactElement(<Routes />)];
},
});
@@ -0,0 +1,83 @@
/*
* 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 React from 'react';
import {
UnifiedThemeProvider,
themes as builtinThemes,
} from '@backstage/theme';
import DarkIcon from '@material-ui/icons/Brightness2';
import LightIcon from '@material-ui/icons/WbSunny';
import {
createExtensionInput,
ThemeBlueprint,
ApiBlueprint,
createApiFactory,
appThemeApiRef,
} from '@backstage/frontend-plugin-api';
import { AppThemeSelector } from '@backstage/core-app-api';
/**
* Contains the themes installed into the app.
*/
export const AppThemeApi = ApiBlueprint.makeWithOverrides({
name: 'app-theme',
inputs: {
themes: createExtensionInput([ThemeBlueprint.dataRefs.theme], {
replaces: [{ id: 'app', input: 'themes' }],
}),
},
factory: (originalFactory, { inputs }) => {
return originalFactory({
factory: createApiFactory(
appThemeApiRef,
AppThemeSelector.createWithStorage(
inputs.themes.map(i => i.get(ThemeBlueprint.dataRefs.theme)),
),
),
});
},
});
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 = ThemeBlueprint.make({
name: 'dark',
params: {
theme: {
id: 'dark',
title: 'Dark Theme',
variant: 'dark',
icon: <DarkIcon />,
Provider: ({ children }) => (
<UnifiedThemeProvider theme={builtinThemes.dark} children={children} />
),
},
},
});
@@ -0,0 +1,50 @@
/*
* 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 {
createComponentExtension,
createExtensionInput,
ApiBlueprint,
createApiFactory,
componentsApiRef,
} from '@backstage/frontend-plugin-api';
// 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.
*/
export const ComponentsApi = ApiBlueprint.makeWithOverrides({
name: 'components',
inputs: {
components: createExtensionInput(
[createComponentExtension.componentDataRef],
{ replaces: [{ id: 'app', input: 'components' }] },
),
},
factory: (originalFactory, { inputs }) => {
return originalFactory({
factory: createApiFactory(
componentsApiRef,
DefaultComponentsApi.fromComponents(
inputs.components.map(i =>
i.get(createComponentExtension.componentDataRef),
),
),
),
});
},
});
@@ -0,0 +1,38 @@
/*
* 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 {
ApiBlueprint,
createApiFactory,
featureFlagsApiRef,
} from '@backstage/frontend-plugin-api';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { LocalStorageFeatureFlags } from '../../../../packages/core-app-api/src/apis/implementations/FeatureFlagsApi/LocalStorageFeatureFlags';
/**
* Contains the shareable icons installed into the app.
*/
export const FeatureFlagsApi = ApiBlueprint.make({
name: 'feature-flags',
params: {
// TODO: properly discovery feature flags, maybe rework the whole thing
factory: createApiFactory({
api: featureFlagsApiRef,
deps: {},
factory: () => new LocalStorageFeatureFlags(),
}),
},
});
+51
View File
@@ -0,0 +1,51 @@
/*
* 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 {
createExtensionInput,
IconBundleBlueprint,
ApiBlueprint,
createApiFactory,
iconsApiRef,
} from '@backstage/frontend-plugin-api';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
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.
*/
export const IconsApi = ApiBlueprint.makeWithOverrides({
name: 'icons',
inputs: {
icons: createExtensionInput([IconBundleBlueprint.dataRefs.icons], {
replaces: [{ id: 'app', input: 'icons' }],
}),
},
factory: (originalFactory, { inputs }) => {
return originalFactory({
factory: createApiFactory(
iconsApiRef,
new DefaultIconsApi(
inputs.icons
.map(i => i.get(IconBundleBlueprint.dataRefs.icons))
.reduce((acc, bundle) => ({ ...acc, ...bundle }), defaultIcons),
),
),
});
},
});
@@ -0,0 +1,56 @@
/*
* 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 {
ApiBlueprint,
TranslationBlueprint,
createApiFactory,
createExtensionInput,
} from '@backstage/frontend-plugin-api';
import {
appLanguageApiRef,
translationApiRef,
} from '@backstage/core-plugin-api/alpha';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { I18nextTranslationApi } from '../../../../packages/core-app-api/src/apis/implementations/TranslationApi/I18nextTranslationApi';
/**
* Contains translations that are installed in the app.
*/
export const TranslationsApi = ApiBlueprint.makeWithOverrides({
name: 'translations',
inputs: {
translations: createExtensionInput(
[TranslationBlueprint.dataRefs.translation],
{ replaces: [{ id: 'app', input: 'translations' }] },
),
},
factory: (originalFactory, { inputs }) => {
return originalFactory({
factory: createApiFactory({
api: translationApiRef,
deps: { languageApi: appLanguageApiRef },
factory: ({ languageApi }) =>
I18nextTranslationApi.create({
languageApi,
resources: inputs.translations.map(i =>
i.get(TranslationBlueprint.dataRefs.translation),
),
}),
}),
});
},
});
+55
View File
@@ -0,0 +1,55 @@
/*
* Copyright 2023 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';
// TODO: Dependency on MUI should be removed from core packages
import Button from '@material-ui/core/Button';
import {
createComponentExtension,
coreComponentRefs,
} 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 '../../../../packages/app-defaults/src/defaults';
export const DefaultProgressComponent = createComponentExtension({
ref: coreComponentRefs.progress,
loader: { sync: () => defaultComponents.Progress },
});
export const DefaultNotFoundErrorPageComponent = createComponentExtension({
ref: coreComponentRefs.notFoundErrorPage,
loader: { sync: () => defaultComponents.NotFoundErrorPage },
});
export const DefaultErrorBoundaryComponent = createComponentExtension({
ref: coreComponentRefs.errorBoundaryFallback,
loader: {
sync: () => props => {
const { plugin, error, resetError } = props;
const title = `Error in ${plugin?.id}`;
return (
<ErrorPanel title={title} error={error} defaultExpanded>
<Button variant="outlined" onClick={resetError}>
Retry
</Button>
</ErrorPanel>
);
},
},
});
+50
View File
@@ -0,0 +1,50 @@
/*
* Copyright 2023 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 { AlertDisplay, OAuthRequestDialog } from '@backstage/core-components';
import { AppRootElementBlueprint } from '@backstage/frontend-plugin-api';
import React from 'react';
export const oauthRequestDialogAppRootElement = AppRootElementBlueprint.make({
namespace: 'app',
name: 'oauth-request-dialog',
params: {
element: <OAuthRequestDialog />,
},
});
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} />,
});
},
});