revert: keep scaffolder on the old layout

Back out the scaffolder NFS header migration for now so the create and related pages stay on the existing layout until the follow-up scaffolder-specific work is ready.

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
Made-with: Cursor
This commit is contained in:
Patrik Oldsberg
2026-03-17 15:52:42 +01:00
parent d5a958fd88
commit f7777a6ec8
18 changed files with 267 additions and 486 deletions
-1
View File
@@ -7,7 +7,6 @@
'@backstage/plugin-catalog-unprocessed-entities': patch
'@backstage/plugin-devtools': patch
'@backstage/plugin-notifications': patch
'@backstage/plugin-scaffolder': patch
'@backstage/plugin-search': patch
'@backstage/plugin-techdocs': patch
'@backstage/plugin-user-settings': patch
-1
View File
@@ -74,7 +74,6 @@
"@backstage/plugin-techdocs-common": "workspace:^",
"@backstage/plugin-techdocs-react": "workspace:^",
"@backstage/types": "workspace:^",
"@backstage/ui": "workspace:^",
"@codemirror/language": "^6.0.0",
"@codemirror/legacy-modes": "^6.1.0",
"@codemirror/view": "^6.0.0",
-2
View File
@@ -727,7 +727,6 @@ export type TemplateListPageProps = {
title?: string;
subtitle?: string;
};
headerVariant?: 'legacy' | 'bui';
};
// @alpha (undocumented)
@@ -743,7 +742,6 @@ export type TemplateWizardPageProps = {
title?: string;
subtitle?: string;
};
headerVariant?: 'legacy' | 'bui';
};
// (No @packageDocumentation comment for this package)
+1 -9
View File
@@ -499,14 +499,7 @@ export type RouterProps = {
TemplateCardComponent?: ComponentType<{
template: TemplateEntityV1beta3;
}>;
TaskPageComponent?: ComponentType<
PropsWithChildren<{
TemplateOutputsComponent?: ComponentType<{
output?: ScaffolderTaskOutput_2;
}>;
headerVariant?: 'legacy' | 'bui';
}>
>;
TaskPageComponent?: ComponentType<PropsWithChildren<{}>>;
EXPERIMENTAL_TemplateOutputsComponent?: ComponentType<{
output?: ScaffolderTaskOutput_2;
}>;
@@ -629,7 +622,6 @@ export const TaskPage: (props: {
TemplateOutputsComponent?: ComponentType<{
output?: ScaffolderTaskOutput_2;
}>;
headerVariant?: 'legacy' | 'bui';
}) => JSX_2.Element;
// @public @deprecated
@@ -14,6 +14,7 @@
* limitations under the License.
*/
import { Page, Header, Content } from '@backstage/core-components';
import { useRouteRef } from '@backstage/core-plugin-api';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
import { FieldExtensionOptions } from '@backstage/plugin-scaffolder-react';
@@ -22,11 +23,9 @@ import { editRouteRef } from '../../../routes';
import { scaffolderTranslationRef } from '../../../translation';
import { CustomFieldExplorer } from './CustomFieldExplorer';
import { ScaffolderPageLayout } from '../../../components/ScaffolderPageLayout';
interface CustomFieldsPageProps {
fieldExtensions?: FieldExtensionOptions<any, any>[];
headerVariant?: 'legacy' | 'bui';
}
export function CustomFieldsPage(props: CustomFieldsPageProps) {
@@ -34,15 +33,16 @@ export function CustomFieldsPage(props: CustomFieldsPageProps) {
const { t } = useTranslationRef(scaffolderTranslationRef);
return (
<ScaffolderPageLayout
themeId="home"
headerVariant={props.headerVariant}
title={t('templateCustomFieldPage.title')}
subtitle={t('templateCustomFieldPage.subtitle')}
type={t('templateIntroPage.title')}
typeLink={editLink()}
>
<CustomFieldExplorer customFieldExtensions={props.fieldExtensions} />
</ScaffolderPageLayout>
<Page themeId="home">
<Header
title={t('templateCustomFieldPage.title')}
subtitle={t('templateCustomFieldPage.subtitle')}
type={t('templateIntroPage.title')}
typeLink={editLink()}
/>
<Content>
<CustomFieldExplorer customFieldExtensions={props.fieldExtensions} />
</Content>
</Page>
);
}
@@ -14,6 +14,7 @@
* limitations under the License.
*/
import { makeStyles } from '@material-ui/core/styles';
import { Content, Header, Page } from '@backstage/core-components';
import { useRouteRef } from '@backstage/core-plugin-api';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
import {
@@ -24,7 +25,6 @@ import {
import { scaffolderTranslationRef } from '../../../translation';
import { editRouteRef } from '../../../routes';
import { TemplateEditor } from './TemplateEditor';
import { ScaffolderPageLayout } from '../../../components/ScaffolderPageLayout';
const useStyles = makeStyles(
{
@@ -40,7 +40,6 @@ interface TemplatePageProps {
fieldExtensions?: FieldExtensionOptions<any, any>[];
layouts?: LayoutOptions[];
formProps?: FormProps;
headerVariant?: 'legacy' | 'bui';
}
export function TemplateEditorPage(props: TemplatePageProps) {
@@ -49,20 +48,20 @@ export function TemplateEditorPage(props: TemplatePageProps) {
const { t } = useTranslationRef(scaffolderTranslationRef);
return (
<ScaffolderPageLayout
themeId="home"
headerVariant={props.headerVariant}
title={t('templateEditorPage.title')}
subtitle={t('templateEditorPage.subtitle')}
type={t('templateIntroPage.title')}
typeLink={editLink()}
contentClassName={classes.content}
>
<TemplateEditor
layouts={props.layouts}
formProps={props.formProps}
fieldExtensions={props.fieldExtensions}
<Page themeId="home">
<Header
title={t('templateEditorPage.title')}
subtitle={t('templateEditorPage.subtitle')}
type={t('templateIntroPage.title')}
typeLink={editLink()}
/>
</ScaffolderPageLayout>
<Content className={classes.content}>
<TemplateEditor
layouts={props.layouts}
formProps={props.formProps}
fieldExtensions={props.fieldExtensions}
/>
</Content>
</Page>
);
}
@@ -19,6 +19,7 @@ import { useNavigate } from 'react-router-dom';
import { makeStyles } from '@material-ui/core/styles';
import { Page, Header, Content } from '@backstage/core-components';
import { useRouteRef } from '@backstage/core-plugin-api';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
import {
@@ -31,7 +32,6 @@ import { editRouteRef } from '../../../routes';
import { scaffolderTranslationRef } from '../../../translation';
import { TemplateFormPreviewer } from './TemplateFormPreviewer';
import { ScaffolderPageLayout } from '../../../components/ScaffolderPageLayout';
const useStyles = makeStyles({
root: {
@@ -44,7 +44,6 @@ interface TemplateFormPageProps {
formProps?: FormProps;
fieldExtensions?: FieldExtensionOptions<any, any>[];
defaultPreviewTemplate?: string;
headerVariant?: 'legacy' | 'bui';
}
export function TemplateFormPage(props: TemplateFormPageProps) {
@@ -58,22 +57,22 @@ export function TemplateFormPage(props: TemplateFormPageProps) {
}, [navigate, editLink]);
return (
<ScaffolderPageLayout
themeId="home"
headerVariant={props.headerVariant}
title={t('templateFormPage.title')}
subtitle={t('templateFormPage.subtitle')}
type={t('templateIntroPage.title')}
typeLink={editLink()}
contentClassName={classes.root}
>
<TemplateFormPreviewer
layouts={props.layouts}
formProps={props.formProps}
customFieldExtensions={props.fieldExtensions}
defaultPreviewTemplate={props.defaultPreviewTemplate}
onClose={handleClose}
<Page themeId="home">
<Header
title={t('templateFormPage.title')}
subtitle={t('templateFormPage.subtitle')}
type={t('templateIntroPage.title')}
typeLink={editLink()}
/>
</ScaffolderPageLayout>
<Content className={classes.root}>
<TemplateFormPreviewer
layouts={props.layouts}
formProps={props.formProps}
customFieldExtensions={props.fieldExtensions}
defaultPreviewTemplate={props.defaultPreviewTemplate}
onClose={handleClose}
/>
</Content>
</Page>
);
}
@@ -14,6 +14,7 @@
* limitations under the License.
*/
import { useCallback } from 'react';
import { Content, Header, Page } from '@backstage/core-components';
import { TemplateEditorIntro } from './TemplateEditorIntro';
import { useNavigate } from 'react-router-dom';
@@ -27,9 +28,8 @@ import {
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
import { scaffolderTranslationRef } from '../../../translation';
import { useTemplateDirectory } from './useTemplateDirectory';
import { ScaffolderPageLayout } from '../../../components/ScaffolderPageLayout';
export function TemplateIntroPage(props: { headerVariant?: 'legacy' | 'bui' }) {
export function TemplateIntroPage() {
const navigate = useNavigate();
const createLink = useRouteRef(rootRouteRef);
const editorLink = useRouteRef(editorRouteRef);
@@ -65,15 +65,16 @@ export function TemplateIntroPage(props: { headerVariant?: 'legacy' | 'bui' }) {
);
return (
<ScaffolderPageLayout
themeId="home"
headerVariant={props.headerVariant}
title={t('templateIntroPage.title')}
type="Scaffolder"
typeLink={createLink()}
subtitle={t('templateIntroPage.subtitle')}
>
<TemplateEditorIntro onSelect={handleSelect} />
</ScaffolderPageLayout>
<Page themeId="home">
<Header
title={t('templateIntroPage.title')}
type="Scaffolder"
typeLink={createLink()}
subtitle={t('templateIntroPage.subtitle')}
/>
<Content>
<TemplateEditorIntro onSelect={handleSelect} />
</Content>
</Page>
);
}
@@ -20,8 +20,11 @@ import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common';
import { useApp, useRouteRef } from '@backstage/core-plugin-api';
import {
Content,
ContentHeader,
DocsIcon,
Header,
Page,
SupportButton,
} from '@backstage/core-components';
import {
@@ -56,7 +59,6 @@ import {
useTranslationRef,
} from '@backstage/core-plugin-api/alpha';
import { scaffolderTranslationRef } from '../../../translation';
import { ScaffolderPageLayout } from '../../../components/ScaffolderPageLayout';
import { buildTechDocsURL } from '@backstage/plugin-techdocs-react';
import {
TECHDOCS_ANNOTATION,
@@ -83,7 +85,6 @@ export type TemplateListPageProps = {
title?: string;
subtitle?: string;
};
headerVariant?: 'legacy' | 'bui';
};
const createGroupsWithOther = (
@@ -182,62 +183,55 @@ export const TemplateListPage = (props: TemplateListPageProps) => {
},
[navigate, templateRoute],
);
const pageActions = (
<>
<RegisterExistingButton
title={t('templateListPage.contentHeader.registerExistingButtonTitle')}
to={registerComponentLink && registerComponentLink()}
/>
<SupportButton>
{t('templateListPage.contentHeader.supportButtonTitle')}
</SupportButton>
</>
);
return (
<EntityListProvider>
<ScaffolderPageLayout
themeId="home"
headerVariant={props.headerVariant}
pageTitleOverride={
headerOptions?.pageTitleOverride ?? t('templateListPage.pageTitle')
}
title={headerOptions?.title ?? t('templateListPage.title')}
subtitle={headerOptions?.subtitle ?? t('templateListPage.subtitle')}
headerActions={
<>
{props.headerVariant === 'bui' ? pageActions : undefined}
<ScaffolderPageContextMenu {...scaffolderPageContextMenuProps} />
</>
}
>
{props.headerVariant !== 'bui' ? (
<ContentHeader>{pageActions}</ContentHeader>
) : null}
<Page themeId="home">
<Header
pageTitleOverride={t('templateListPage.pageTitle')}
title={t('templateListPage.title')}
subtitle={t('templateListPage.subtitle')}
{...headerOptions}
>
<ScaffolderPageContextMenu {...scaffolderPageContextMenuProps} />
</Header>
<Content>
<ContentHeader>
<RegisterExistingButton
title={t(
'templateListPage.contentHeader.registerExistingButtonTitle',
)}
to={registerComponentLink && registerComponentLink()}
/>
<SupportButton>
{t('templateListPage.contentHeader.supportButtonTitle')}
</SupportButton>
</ContentHeader>
<CatalogFilterLayout>
<CatalogFilterLayout.Filters>
<EntitySearchBar />
<EntityKindPicker initialFilter="template" hidden />
<UserListPicker
initialFilter="all"
availableFilters={['all', 'starred']}
/>
<TemplateCategoryPicker />
<EntityTagPicker />
<EntityOwnerPicker />
</CatalogFilterLayout.Filters>
<CatalogFilterLayout.Content>
<TemplateGroups
groups={groups}
templateFilter={templateFilter}
TemplateCardComponent={TemplateCardComponent}
onTemplateSelected={onTemplateSelected}
additionalLinksForEntity={additionalLinksForEntity}
/>
</CatalogFilterLayout.Content>
</CatalogFilterLayout>
</ScaffolderPageLayout>
<CatalogFilterLayout>
<CatalogFilterLayout.Filters>
<EntitySearchBar />
<EntityKindPicker initialFilter="template" hidden />
<UserListPicker
initialFilter="all"
availableFilters={['all', 'starred']}
/>
<TemplateCategoryPicker />
<EntityTagPicker />
<EntityOwnerPicker />
</CatalogFilterLayout.Filters>
<CatalogFilterLayout.Content>
<TemplateGroups
groups={groups}
templateFilter={templateFilter}
TemplateCardComponent={TemplateCardComponent}
onTemplateSelected={onTemplateSelected}
additionalLinksForEntity={additionalLinksForEntity}
/>
</CatalogFilterLayout.Content>
</CatalogFilterLayout>
</Content>
</Page>
</EntityListProvider>
);
};
@@ -41,7 +41,7 @@ import {
useTemplateParameterSchema,
} from '@backstage/plugin-scaffolder-react/alpha';
import { JsonValue } from '@backstage/types';
import { Progress } from '@backstage/core-components';
import { Header, Page, Progress } from '@backstage/core-components';
import {
rootRouteRef,
@@ -53,7 +53,6 @@ import { scaffolderTranslationRef } from '../../../translation';
import { TemplateWizardPageContextMenu } from './TemplateWizardPageContextMenu';
import { useFormDecorators } from '../../hooks';
import { ScaffolderPageLayout } from '../../../components/ScaffolderPageLayout';
/**
* @alpha
@@ -70,7 +69,6 @@ export type TemplateWizardPageProps = {
title?: string;
subtitle?: string;
};
headerVariant?: 'legacy' | 'bui';
};
export const TemplateWizardPage = (props: TemplateWizardPageProps) => {
@@ -138,24 +136,21 @@ export const TemplateWizardPage = (props: TemplateWizardPageProps) => {
return (
<AnalyticsContext attributes={{ entityRef: templateRef }}>
<ScaffolderPageLayout
themeId="website"
withContent={false}
headerVariant={props.headerVariant}
pageTitleOverride={
props.headerOptions?.pageTitleOverride ??
(manifest?.title
? t('templateWizardPage.templateWithTitle', {
templateTitle: manifest.title,
})
: t('templateWizardPage.pageTitle'))
}
title={props.headerOptions?.title ?? t('templateWizardPage.title')}
subtitle={
props.headerOptions?.subtitle ?? t('templateWizardPage.subtitle')
}
headerActions={<TemplateWizardPageContextMenu editUrl={editUrl} />}
>
<Page themeId="website">
<Header
pageTitleOverride={
manifest?.title
? t('templateWizardPage.templateWithTitle', {
templateTitle: manifest.title,
})
: t('templateWizardPage.pageTitle')
}
title={t('templateWizardPage.title')}
subtitle={t('templateWizardPage.subtitle')}
{...props.headerOptions}
>
<TemplateWizardPageContextMenu editUrl={editUrl} />
</Header>
{isCreating && <Progress />}
<Workflow
namespace={namespace}
@@ -167,7 +162,7 @@ export const TemplateWizardPage = (props: TemplateWizardPageProps) => {
formProps={props.formProps}
layouts={props.layouts}
/>
</ScaffolderPageLayout>
</Page>
</AnalyticsContext>
);
};
+1 -1
View File
@@ -59,7 +59,7 @@ export const scaffolderPage = PageBlueprint.makeWithOverrides({
const formFields = [...apiFormFields, ...loadedFormFields];
return import('../components/Router/Router').then(m => (
<m.InternalRouter formFields={formFields} headerVariant="bui" />
<m.InternalRouter formFields={formFields} />
));
},
});
@@ -31,10 +31,13 @@ import SearchIcon from '@material-ui/icons/Search';
import { useApi, useRouteRef } from '@backstage/core-plugin-api';
import {
Content,
EmptyState,
ErrorPanel,
Header,
Link,
MarkdownContent,
Page,
Progress,
} from '@backstage/core-components';
import { ScaffolderPageContextMenu } from '@backstage/plugin-scaffolder-react/alpha';
@@ -49,7 +52,6 @@ import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
import { scaffolderTranslationRef } from '../../translation';
import { Expanded, RenderSchema, SchemaRenderContext } from '../RenderSchema';
import { ScaffolderUsageExamplesTable } from '../ScaffolderUsageExamplesTable';
import { ScaffolderPageLayout } from '../ScaffolderPageLayout';
const useStyles = makeStyles(theme => ({
code: {
@@ -240,7 +242,6 @@ export type ActionsPageProps = {
create?: boolean;
templatingExtensions?: boolean;
};
headerVariant?: 'legacy' | 'bui';
};
export const ActionsPage = (props: ActionsPageProps) => {
@@ -272,17 +273,17 @@ export const ActionsPage = (props: ActionsPageProps) => {
};
return (
<ScaffolderPageLayout
themeId="home"
headerVariant={props.headerVariant}
pageTitleOverride={t('actionsPage.pageTitle')}
title={t('actionsPage.title')}
subtitle={t('actionsPage.subtitle')}
headerActions={
<Page themeId="home">
<Header
pageTitleOverride={t('actionsPage.pageTitle')}
title={t('actionsPage.title')}
subtitle={t('actionsPage.subtitle')}
>
<ScaffolderPageContextMenu {...scaffolderPageContextMenuProps} />
}
>
<ActionPageContent />
</ScaffolderPageLayout>
</Header>
<Content>
<ActionPageContent />
</Content>
</Page>
);
};
@@ -14,9 +14,12 @@
* limitations under the License.
*/
import {
Content,
EmptyState,
ErrorPanel,
Header,
Link,
Page,
Progress,
Table,
} from '@backstage/core-components';
@@ -45,7 +48,6 @@ import { ScaffolderPageContextMenu } from '@backstage/plugin-scaffolder-react/al
import { useNavigate } from 'react-router-dom';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
import { scaffolderTranslationRef } from '../../translation';
import { ScaffolderPageLayout } from '../ScaffolderPageLayout';
export interface MyTaskPageProps {
initiallySelectedFilter?: 'owned' | 'all';
@@ -55,7 +57,6 @@ export interface MyTaskPageProps {
create?: boolean;
templatingExtensions?: boolean;
};
headerVariant?: 'legacy' | 'bui';
}
const ListTaskPageContent = (props: MyTaskPageProps) => {
@@ -199,17 +200,17 @@ export const ListTasksPage = (props: MyTaskPageProps) => {
: undefined,
};
return (
<ScaffolderPageLayout
themeId="home"
headerVariant={props.headerVariant}
pageTitleOverride={t('listTaskPage.pageTitle')}
title={t('listTaskPage.title')}
subtitle={t('listTaskPage.subtitle')}
headerActions={
<Page themeId="home">
<Header
pageTitleOverride={t('listTaskPage.pageTitle')}
title={t('listTaskPage.title')}
subtitle={t('listTaskPage.subtitle')}
>
<ScaffolderPageContextMenu {...scaffolderPageContextMenuProps} />
}
>
<ListTaskPageContent {...props} />
</ScaffolderPageLayout>
</Header>
<Content>
<ListTaskPageContent {...props} />
</Content>
</Page>
);
};
@@ -20,7 +20,7 @@ import {
useMemo,
useState,
} from 'react';
import { ErrorPanel } from '@backstage/core-components';
import { Content, ErrorPanel, Header, Page } from '@backstage/core-components';
import { useNavigate, useParams } from 'react-router-dom';
import Box from '@material-ui/core/Box';
import Button from '@material-ui/core/Button';
@@ -57,7 +57,6 @@ import { scaffolderTranslationRef } from '../../translation';
import { entityPresentationApiRef } from '@backstage/plugin-catalog-react';
import { default as reactUseAsync } from 'react-use/esm/useAsync';
import { stringifyEntityRef } from '@backstage/catalog-model';
import { ScaffolderPageLayout } from '../ScaffolderPageLayout';
const useStyles = makeStyles(theme => ({
contentWrapper: {
@@ -87,7 +86,6 @@ export const OngoingTask = (props: {
TemplateOutputsComponent?: ComponentType<{
output?: ScaffolderTaskOutput;
}>;
headerVariant?: 'legacy' | 'bui';
}) => {
// todo(blam): check that task Id actually exists, and that it's valid. otherwise redirect to something more useful.
const { taskId } = useParams();
@@ -108,7 +106,9 @@ export const OngoingTask = (props: {
taskId,
}}
>
<OngoingTaskContent {...props} />
<Page themeId="website">
<OngoingTaskContent {...props} />
</Page>
</AnalyticsContext>
);
};
@@ -117,7 +117,6 @@ function OngoingTaskContent(props: {
TemplateOutputsComponent?: ComponentType<{
output?: ScaffolderTaskOutput;
}>;
headerVariant?: 'legacy' | 'bui';
}) {
const { taskId } = useParams();
const templateRouteRef = useRouteRef(selectedTemplateRouteRef);
@@ -244,24 +243,23 @@ function OngoingTaskContent(props: {
!cancelEnabled || cancelStatus !== 'not-executed' || !canCancelTask;
return (
<ScaffolderPageLayout
themeId="website"
headerVariant={props.headerVariant}
pageTitleOverride={
presentation
? t('ongoingTask.pageTitle.hasTemplateName', {
templateName: presentation.primaryTitle,
})
: t('ongoingTask.pageTitle.noTemplateName')
}
title={
<div>
{t('ongoingTask.title')}{' '}
<code>{presentation ? presentation.primaryTitle : ''}</code>
</div>
}
subtitle={t('ongoingTask.subtitle', { taskId: taskId as string })}
headerActions={
<>
<Header
pageTitleOverride={
presentation
? t('ongoingTask.pageTitle.hasTemplateName', {
templateName: presentation.primaryTitle,
})
: t('ongoingTask.pageTitle.noTemplateName')
}
title={
<div>
{t('ongoingTask.title')}{' '}
<code>{presentation ? presentation.primaryTitle : ''}</code>
</div>
}
subtitle={t('ongoingTask.subtitle', { taskId: taskId as string })}
>
<ContextMenu
cancelEnabled={cancelEnabled}
canRetry={canRetry}
@@ -276,89 +274,89 @@ function OngoingTaskContent(props: {
onCancel={triggerCancel}
isCancelButtonDisabled={isCancelButtonDisabled}
/>
}
contentClassName={classes.contentWrapper}
>
{taskStream.error ? (
</Header>
<Content className={classes.contentWrapper}>
{taskStream.error ? (
<Box paddingBottom={2}>
<ErrorPanel
error={taskStream.error}
titleFormat="markdown"
title={taskStream.error.message}
/>
</Box>
) : null}
<Box paddingBottom={2}>
<ErrorPanel
error={taskStream.error}
titleFormat="markdown"
title={taskStream.error.message}
<TaskSteps
steps={steps}
activeStep={activeStep}
isComplete={taskStream.completed}
isError={Boolean(taskStream.error)}
/>
</Box>
) : null}
<Box paddingBottom={2}>
<TaskSteps
steps={steps}
activeStep={activeStep}
isComplete={taskStream.completed}
isError={Boolean(taskStream.error)}
/>
</Box>
<Outputs output={taskStream.output} />
<Outputs output={taskStream.output} />
{buttonBarVisible ? (
<Box paddingBottom={2}>
<Paper>
<Box padding={2}>
<div className={classes.buttonBar}>
<Button
className={classes.cancelButton}
disabled={
!cancelEnabled ||
(cancelStatus !== 'not-executed' && !isRetryableTask) ||
!canCancelTask
}
onClick={triggerCancel}
data-testid="cancel-button"
>
{t('ongoingTask.cancelButtonTitle')}
</Button>
{isRetryableTask && (
{buttonBarVisible ? (
<Box paddingBottom={2}>
<Paper>
<Box padding={2}>
<div className={classes.buttonBar}>
<Button
className={classes.retryButton}
disabled={cancelEnabled || !canRetry}
onClick={triggerRetry}
data-testid="retry-button"
className={classes.cancelButton}
disabled={
!cancelEnabled ||
(cancelStatus !== 'not-executed' && !isRetryableTask) ||
!canCancelTask
}
onClick={triggerCancel}
data-testid="cancel-button"
>
{t('ongoingTask.retryButtonTitle')}
{t('ongoingTask.cancelButtonTitle')}
</Button>
)}
<Button
className={classes.logsVisibilityButton}
color="primary"
variant="outlined"
onClick={() => setLogVisibleState(!logsVisible)}
>
{logsVisible
? t('ongoingTask.hideLogsButtonTitle')
: t('ongoingTask.showLogsButtonTitle')}
</Button>
<Button
variant="contained"
color="primary"
disabled={cancelEnabled || !canStartOver}
onClick={startOver}
data-testid="start-over-button"
>
{t('ongoingTask.startOverButtonTitle')}
</Button>
</div>
{isRetryableTask && (
<Button
className={classes.retryButton}
disabled={cancelEnabled || !canRetry}
onClick={triggerRetry}
data-testid="retry-button"
>
{t('ongoingTask.retryButtonTitle')}
</Button>
)}
<Button
className={classes.logsVisibilityButton}
color="primary"
variant="outlined"
onClick={() => setLogVisibleState(!logsVisible)}
>
{logsVisible
? t('ongoingTask.hideLogsButtonTitle')
: t('ongoingTask.showLogsButtonTitle')}
</Button>
<Button
variant="contained"
color="primary"
disabled={cancelEnabled || !canStartOver}
onClick={startOver}
data-testid="start-over-button"
>
{t('ongoingTask.startOverButtonTitle')}
</Button>
</div>
</Box>
</Paper>
</Box>
) : null}
{logsVisible ? (
<Paper style={{ height: '100%' }}>
<Box padding={2} height="100%">
<TaskLogStream logs={taskStream.stepLogs} />
</Box>
</Paper>
</Box>
) : null}
{logsVisible ? (
<Paper style={{ height: '100%' }}>
<Box padding={2} height="100%">
<TaskLogStream logs={taskStream.stepLogs} />
</Box>
</Paper>
) : null}
</ScaffolderPageLayout>
) : null}
</Content>
</>
);
}
@@ -77,14 +77,7 @@ export type RouterProps = {
TemplateCardComponent?: ComponentType<{
template: TemplateEntityV1beta3;
}>;
TaskPageComponent?: ComponentType<
PropsWithChildren<{
TemplateOutputsComponent?: ComponentType<{
output?: ScaffolderTaskOutput;
}>;
headerVariant?: 'legacy' | 'bui';
}>
>;
TaskPageComponent?: ComponentType<PropsWithChildren<{}>>;
EXPERIMENTAL_TemplateOutputsComponent?: ComponentType<{
output?: ScaffolderTaskOutput;
}>;
@@ -124,7 +117,6 @@ export const InternalRouter = (
props: PropsWithChildren<
RouterProps & {
formFields?: Array<FormField>;
headerVariant?: 'legacy' | 'bui';
}
>,
) => {
@@ -170,7 +162,6 @@ export const InternalRouter = (
groups={props.groups}
templateFilter={props.templateFilter}
headerOptions={props.headerOptions}
headerVariant={props.headerVariant}
/>
}
/>
@@ -184,7 +175,6 @@ export const InternalRouter = (
layouts={customLayouts}
components={{ ReviewStepComponent }}
formProps={props.formProps}
headerVariant={props.headerVariant}
/>
</SecretsContextProvider>
}
@@ -194,7 +184,6 @@ export const InternalRouter = (
element={
<TaskPageComponent
TemplateOutputsComponent={TemplateOutputsComponent}
headerVariant={props.headerVariant}
/>
}
/>
@@ -203,7 +192,7 @@ export const InternalRouter = (
element={
<RequirePermission permission={templateManagementPermission}>
<SecretsContextProvider>
<TemplateIntroPage headerVariant={props.headerVariant} />
<TemplateIntroPage />
</SecretsContextProvider>
</RequirePermission>
}
@@ -213,10 +202,7 @@ export const InternalRouter = (
element={
<RequirePermission permission={templateManagementPermission}>
<SecretsContextProvider>
<CustomFieldsPage
fieldExtensions={fieldExtensions}
headerVariant={props.headerVariant}
/>
<CustomFieldsPage fieldExtensions={fieldExtensions} />
</SecretsContextProvider>
</RequirePermission>
}
@@ -230,7 +216,6 @@ export const InternalRouter = (
layouts={customLayouts}
formProps={props.formProps}
fieldExtensions={fieldExtensions}
headerVariant={props.headerVariant}
/>
</SecretsContextProvider>
</RequirePermission>
@@ -239,21 +224,11 @@ export const InternalRouter = (
<Route
path={actionsRouteRef.path}
element={
<ActionsPage
contextMenu={props.contextMenu}
headerVariant={props.headerVariant}
/>
}
element={<ActionsPage contextMenu={props.contextMenu} />}
/>
<Route
path={scaffolderListTaskRouteRef.path}
element={
<ListTasksPage
contextMenu={props.contextMenu}
headerVariant={props.headerVariant}
/>
}
element={<ListTasksPage contextMenu={props.contextMenu} />}
/>
<Route
path={editorRouteRef.path}
@@ -264,7 +239,6 @@ export const InternalRouter = (
layouts={customLayouts}
formProps={props.formProps}
fieldExtensions={fieldExtensions}
headerVariant={props.headerVariant}
/>
</SecretsContextProvider>
</RequirePermission>
@@ -272,12 +246,7 @@ export const InternalRouter = (
/>
<Route
path={templatingExtensionsRouteRef.path}
element={
<TemplatingExtensionsPage
contextMenu={props.contextMenu}
headerVariant={props.headerVariant}
/>
}
element={<TemplatingExtensionsPage contextMenu={props.contextMenu} />}
/>
<Route path="*" element={<NotFoundErrorPage />} />
</Routes>
@@ -1,50 +0,0 @@
/*
* Copyright 2026 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 { renderInTestApp } from '@backstage/test-utils';
import { screen } from '@testing-library/react';
import { ScaffolderPageLayout } from './ScaffolderPageLayout';
describe('ScaffolderPageLayout', () => {
it('preserves BUI titles, subtitles, and breadcrumb links', async () => {
await renderInTestApp(
<ScaffolderPageLayout
headerVariant="bui"
pageTitleOverride="Task page"
title={
<div>
Task <code>My template</code>
</div>
}
subtitle="Task ID: 123"
type="Scaffolder"
typeLink="/create"
>
<div>Body content</div>
</ScaffolderPageLayout>,
);
expect(
screen.getByRole('heading', { name: 'Task page' }),
).toBeInTheDocument();
expect(screen.getByText('My template')).toBeInTheDocument();
expect(screen.getByText('Task ID: 123')).toBeInTheDocument();
expect(screen.getByRole('link', { name: 'Scaffolder' })).toHaveAttribute(
'href',
'/create',
);
});
});
@@ -1,115 +0,0 @@
/*
* Copyright 2026 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 Box from '@material-ui/core/Box';
import { Content, Header, Page } from '@backstage/core-components';
import { HeaderPage } from '@backstage/ui';
import type { ReactNode } from 'react';
import { useEffect } from 'react';
type HeaderVariant = 'legacy' | 'bui';
type ScaffolderPageLayoutProps = {
headerVariant?: HeaderVariant;
themeId?: string;
title?: ReactNode;
subtitle?: ReactNode;
pageTitleOverride?: string;
type?: string;
typeLink?: string;
headerActions?: ReactNode;
contentClassName?: string;
withContent?: boolean;
children?: ReactNode;
};
export const ScaffolderPageLayout = (props: ScaffolderPageLayoutProps) => {
const {
headerVariant = 'legacy',
themeId = 'home',
title,
subtitle,
pageTitleOverride,
type,
typeLink,
headerActions,
contentClassName,
withContent = true,
children,
} = props;
const pageContent = withContent ? (
<Content className={contentClassName}>{children}</Content>
) : (
children
);
useEffect(() => {
if (!pageTitleOverride) {
return;
}
document.title = pageTitleOverride;
}, [pageTitleOverride]);
if (headerVariant === 'bui') {
let buiTitle: string | undefined;
if (typeof title === 'string') {
buiTitle = title;
} else if (pageTitleOverride) {
buiTitle = pageTitleOverride;
} else if (typeof subtitle === 'string') {
buiTitle = subtitle;
}
const breadcrumbs =
type && typeLink ? [{ label: type, href: typeLink }] : undefined;
const customTitle = typeof title === 'string' ? undefined : title;
const hasHeaderDetails = Boolean(customTitle) || Boolean(subtitle);
return (
<Page themeId={themeId}>
<HeaderPage
title={buiTitle}
breadcrumbs={breadcrumbs}
customActions={headerActions}
/>
{hasHeaderDetails && (
<Box mt={2}>
{customTitle}
{subtitle && <Box mt={customTitle ? 1 : 0}>{subtitle}</Box>}
</Box>
)}
{pageContent}
</Page>
);
}
return (
<Page themeId={themeId}>
<Header
pageTitleOverride={pageTitleOverride}
title={title}
subtitle={subtitle}
type={type}
typeLink={typeLink}
>
{headerActions}
</Header>
{pageContent}
</Page>
);
};
@@ -29,9 +29,12 @@ import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
import { scaffolderTranslationRef } from '../../translation';
import {
Content,
EmptyState,
ErrorPanel,
Header,
Link,
Page,
Progress,
} from '@backstage/core-components';
import { scaffolderApiRef } from '@backstage/plugin-scaffolder-react';
@@ -65,7 +68,6 @@ import {
TemplateGlobalFunctions,
TemplateGlobalValues,
} from './TemplateGlobals';
import { ScaffolderPageLayout } from '../ScaffolderPageLayout';
const useStyles = makeStyles(theme => ({
code: {
@@ -301,7 +303,6 @@ export type TemplatingExtensionsPageProps = {
tasks?: boolean;
create?: boolean;
};
headerVariant?: 'legacy' | 'bui';
};
export const TemplatingExtensionsPage = (
@@ -336,17 +337,17 @@ export const TemplatingExtensionsPage = (
const { t } = useTranslationRef(scaffolderTranslationRef);
return (
<ScaffolderPageLayout
themeId="home"
headerVariant={props.headerVariant}
pageTitleOverride={t('templatingExtensions.pageTitle')}
title={t('templatingExtensions.title')}
subtitle={t('templatingExtensions.subtitle')}
headerActions={
<Page themeId="home">
<Header
pageTitleOverride={t('templatingExtensions.pageTitle')}
title={t('templatingExtensions.title')}
subtitle={t('templatingExtensions.subtitle')}
>
<ScaffolderPageContextMenu {...scaffolderPageContextMenuProps} />
}
>
<TemplatingExtensionsPageContent linkLocal />
</ScaffolderPageLayout>
</Header>
<Content>
<TemplatingExtensionsPageContent linkLocal />
</Content>
</Page>
);
};