Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
'@red-hat-developer-hub/backstage-plugin-bulk-import': minor
---

Add configurable instructions section for bulk import workflow

This change introduces a fully configurable "Import to Red Hat Developer Hub" instructions section that allows administrators to customize the workflow steps displayed to users.

**New Features:**

- **Configurable Steps**: Define custom workflow steps via `app-config.yaml` with custom text and icons
- **Icon Support**: Support for both built-in theme-aware icons and custom URL-based icons
- **Dynamic Layout**: Steps automatically adjust width for optimal space usage (≤6 steps fill width, >6 steps scroll horizontally)
- **User Preferences**: Collapsed/expanded state persisted in localStorage per user
- **Universal Display**: Instructions section now shows for both PR flow and scaffolder flow
- **Smart Hiding**: Section automatically hides when no steps are configured

**Configuration Schema:**

```yaml
bulkImport:
# Enable/disable the instructions section (default: true)
instructionsEnabled: true

# Default expanded state (default: true)
instructionsDefaultExpanded: true

# Custom workflow steps
instructionsSteps:
- id: 'step1'
text: 'Choose your source control platform'
icon:
type: 'builtin' # or "url"
source: 'approval-tool' # icon name or URL
- id: 'step2'
text: 'Configure without icon'
# Steps without icons show text only
```
60 changes: 60 additions & 0 deletions workspaces/bulk-import/plugins/bulk-import/config.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,65 @@ export interface Config {
* @visibility frontend
*/
importAPI?: 'open-pull-requests' | 'scaffolder';

/**
* The name of the scaffolder template to execute for importing a repository.
* @visibility backend
*/
importTemplate?: string;

/**
* Whether to show the instructions section
* @default true
* @visibility frontend
*/
instructionsEnabled?: boolean;

/**
* Whether the section should be expanded by default
* @default true
* @visibility frontend
*/
instructionsDefaultExpanded?: boolean;

/**
* Array of steps to display in the instructions section
* If not provided, uses the default built-in steps
* Users can define any number of custom steps
* @visibility frontend
* @deepVisibility frontend
*/
instructionsSteps?: Array<{
/**
* Unique identifier for the step
* @visibility frontend
*/
id: string;

/**
* Display text for the step
* @visibility frontend
*/
text: string;

/**
* Icon configuration
* @visibility frontend
*/
icon?: {
/**
* Icon type: 'builtin' for predefined icons, 'url' for custom images
* @visibility frontend
*/
type: 'builtin' | 'url';

/**
* For builtin: icon name (e.g., 'approval-tool', 'choose-repositories', 'generate-cataloginfo', 'edit-pullrequest', 'track-status')
* For url: full URL to the icon image
* @visibility frontend
*/
source: string;
};
}>;
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,20 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { screen } from '@testing-library/react';

import { bulkImportApiRef } from '../../api/BulkImportBackendClient';
import { useNumberOfApprovalTools, useRepositories } from '../../hooks';
import {
useInstructionsConfig,
useInstructionsPreference,
useNumberOfApprovalTools,
useRepositories,
} from '../../hooks';
import { useImportFlow } from '../../hooks/useImportFlow';
import { AddRepositoriesPage } from './AddRepositoriesPage';

jest.mock('../../hooks', () => ({
useNumberOfApprovalTools: jest.fn(),
useRepositories: jest.fn(),
useInstructionsConfig: jest.fn(),
useInstructionsPreference: jest.fn(),
}));

jest.mock('../../hooks/useImportFlow', () => ({
Expand Down Expand Up @@ -126,6 +133,31 @@ describe('AddRepositoriesPage', () => {
data: [],
error: null,
});
(useInstructionsConfig as jest.Mock).mockReturnValue({
enabled: true,
defaultExpanded: true,
steps: [
{
id: 'step1',
text: 'Choose your source control platform',
icon: { type: 'builtin', source: 'approval-tool' },
},
{
id: 'step2',
text: 'Browse and select repositories',
icon: { type: 'builtin', source: 'choose-repositories' },
},
{
id: 'step5',
text: 'Review and edit pull requests',
icon: { type: 'builtin', source: 'edit-pullrequest' },
},
],
});
(useInstructionsPreference as jest.Mock).mockReturnValue([
true, // isExpanded
jest.fn(), // setExpanded
]);
});

it('should render page with correct title', async () => {
Expand All @@ -140,9 +172,7 @@ describe('AddRepositoriesPage', () => {

// Instructions section should be shown for pull request flow
expect(
screen.getByText(
'Choose a source control tool for pull request creation',
),
screen.getByText('Choose your source control platform'),
).toBeInTheDocument();
});

Expand All @@ -151,12 +181,10 @@ describe('AddRepositoriesPage', () => {

// All steps should be shown for pull request flow (both GitHub and GitLab)
expect(
screen.getByText(
'Choose a source control tool for pull request creation',
),
screen.getByText('Choose your source control platform'),
).toBeInTheDocument();
expect(
screen.getByText('View the pull/merge request details'),
screen.getByText('Review and edit pull requests'),
).toBeInTheDocument();
});

Expand All @@ -173,30 +201,23 @@ describe('AddRepositoriesPage', () => {
screen.queryByText('Import to Red Hat Developer Hub'),
).not.toBeInTheDocument();
expect(
screen.queryByText(
'Choose a source control tool for pull request creation',
),
screen.queryByText('Choose your source control platform'),
).not.toBeInTheDocument();

// Form should still be rendered (it will show missing configurations)
expect(screen.getByTestId('add-repositories-form')).toBeInTheDocument();
});

it('should hide instructions section for scaffolder flow', async () => {
it('should show instructions section for scaffolder flow', async () => {
// Override default to test scaffolder flow
(useImportFlow as jest.Mock).mockReturnValue('scaffolder');

await renderWithProviders(<AddRepositoriesPage />);

// Instructions section should be hidden for scaffolder flow
expect(
screen.queryByText('Import to Red Hat Developer Hub'),
).not.toBeInTheDocument();
// Instructions section should now be shown for scaffolder flow since it's customizable
expect(
screen.queryByText(
'Choose a source control tool for pull request creation',
),
).not.toBeInTheDocument();
screen.getByText('Import to Red Hat Developer Hub'),
).toBeInTheDocument();

// Form should still be rendered
expect(screen.getByTestId('add-repositories-form')).toBeInTheDocument();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,26 +19,17 @@ import { useState } from 'react';
import { Content, Header, Page, Progress } from '@backstage/core-components';
import { usePermission } from '@backstage/plugin-permission-react';

import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import Accordion from '@mui/material/Accordion';
import AccordionDetails from '@mui/material/AccordionDetails';
import AccordionSummary from '@mui/material/AccordionSummary';
import Alert from '@mui/material/Alert';
import AlertTitle from '@mui/material/AlertTitle';
import { useTheme } from '@mui/material/styles';
import Typography from '@mui/material/Typography';

import { bulkImportPermission } from '@red-hat-developer-hub/backstage-plugin-bulk-import-common';

import { useNumberOfApprovalTools } from '../../hooks';
import { useImportFlow } from '../../hooks/useImportFlow';
import { useTranslation } from '../../hooks/useTranslation';
import { ImportFlow } from '../../types';
import { AddRepositoriesForm } from './AddRepositoriesForm';
import { Illustrations } from './Illustrations';
import { ConfigurableInstructions } from './ConfigurableInstructions';

export const AddRepositoriesPage = () => {
const theme = useTheme();
const { t } = useTranslation();
const [formError, setFormError] = useState<any>(null);

Expand All @@ -48,12 +39,10 @@ export const AddRepositoriesPage = () => {
});

const { numberOfApprovalTools } = useNumberOfApprovalTools();
const importFlow = useImportFlow();

// Show instructions section only for pull request flow, hide for scaffolder flow
// Also hide if no integrations are configured (missing configurations)
const showInstructionsSection =
importFlow === ImportFlow.OpenPullRequests && numberOfApprovalTools > 0;
// Show instructions section for all flows now that it's customizable
// Only hide if no integrations are configured (missing configurations)
const showInstructionsSection = numberOfApprovalTools > 0;

const showContent = () => {
if (bulkImportViewPermissionResult.loading) {
Expand All @@ -63,69 +52,7 @@ export const AddRepositoriesPage = () => {
return (
<>
{showInstructionsSection && !formError && (
<div style={{ padding: '24px' }}>
<Accordion defaultExpanded>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
id="add-repository-summary"
>
<Typography variant="h5">
{t('page.importEntitiesSubtitle')}
</Typography>
</AccordionSummary>
<AccordionDetails
sx={{
flexDirection: 'row',
display: 'flex',
justifyContent: 'space-around',
overflow: 'auto',
}}
>
{numberOfApprovalTools > 1 && (
<Illustrations
iconClassname={
theme.palette.mode === 'dark'
? 'icon-approval-tool-white'
: 'icon-approval-tool-black'
}
iconText={t('steps.chooseApprovalTool')}
/>
)}
<Illustrations
iconClassname={
theme.palette.mode === 'dark'
? 'icon-choose-repositories-white'
: 'icon-choose-repositories-black'
}
iconText={t('steps.chooseRepositories')}
/>
<Illustrations
iconClassname={
theme.palette.mode === 'dark'
? 'icon-generate-cataloginfo-white'
: 'icon-generate-cataloginfo-black'
}
iconText={t('steps.generateCatalogInfo')}
/>
<Illustrations
iconClassname={
theme.palette.mode === 'dark'
? 'icon-edit-pullrequest-white'
: 'icon-edit-pullrequest-black'
}
iconText={t('steps.editPullRequest')}
/>
<Illustrations
iconClassname={
theme.palette.mode === 'dark'
? 'icon-track-status-white'
: 'icon-track-status-black'
}
iconText={t('steps.trackStatus')}
/>
</AccordionDetails>
</Accordion>
</div>
<ConfigurableInstructions />
)}
<AddRepositoriesForm onErrorChange={setFormError} />
</>
Expand Down
Loading