(null);
@@ -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) {
@@ -63,69 +52,7 @@ export const AddRepositoriesPage = () => {
return (
<>
{showInstructionsSection && !formError && (
-
-
- }
- id="add-repository-summary"
- >
-
- {t('page.importEntitiesSubtitle')}
-
-
-
- {numberOfApprovalTools > 1 && (
-
- )}
-
-
-
-
-
-
-
+
)}
>
diff --git a/workspaces/bulk-import/plugins/bulk-import/src/components/AddRepositories/ConfigurableInstructions.tsx b/workspaces/bulk-import/plugins/bulk-import/src/components/AddRepositories/ConfigurableInstructions.tsx
new file mode 100644
index 0000000000..846d00f47a
--- /dev/null
+++ b/workspaces/bulk-import/plugins/bulk-import/src/components/AddRepositories/ConfigurableInstructions.tsx
@@ -0,0 +1,231 @@
+/*
+ * Copyright Red Hat, Inc.
+ *
+ * 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 { useMemo } from '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 Box from '@mui/material/Box';
+import { useTheme } from '@mui/material/styles';
+import Typography from '@mui/material/Typography';
+
+import { useInstructionsConfig, useInstructionsPreference } from '../../hooks';
+import { useTranslation } from '../../hooks/useTranslation';
+import { Illustrations } from './Illustrations';
+
+interface ConfigurableInstructionsProps {
+ // No props needed - everything comes from configuration
+}
+
+/**
+ * Enhanced Illustrations component that supports custom URL icons
+ */
+const ConfigurableIllustration = ({
+ iconClassname,
+ iconText,
+}: {
+ iconClassname: string;
+ iconText: string;
+}) => {
+ // If no icon is configured, show only text
+ if (!iconClassname) {
+ return (
+
+ {/* Empty space to maintain consistent layout with icon steps */}
+
+ {/* Empty space where icon would be */}
+
+
+ {iconText}
+
+
+ );
+ }
+
+ // Check if this is a custom URL icon
+ const isCustomUrl = iconClassname.startsWith('custom-url:');
+
+ if (isCustomUrl) {
+ const imageUrl = iconClassname.replace('custom-url:', '');
+ return (
+
+
+
+
+
+ {iconText}
+
+
+ );
+ }
+
+ // Use the existing Illustrations component for built-in icons
+ return ;
+};
+
+/**
+ * Configurable instructions component that displays the "How does it work" section
+ * based on configuration from app-config.yaml and user preferences
+ */
+export const ConfigurableInstructions: React.FC<
+ ConfigurableInstructionsProps
+> = () => {
+ const theme = useTheme();
+ const { t } = useTranslation();
+ const config = useInstructionsConfig();
+ const [isExpanded, setExpanded] = useInstructionsPreference(
+ config.defaultExpanded,
+ );
+
+ // Build the list of steps based on configuration from app-config.yaml
+ const steps = useMemo(() => {
+ return config.steps.map(configStep => {
+ let iconClassname = '';
+
+ if (configStep.icon) {
+ if (configStep.icon.type === 'builtin') {
+ // For builtin icons, construct the classname based on theme
+ const iconName = configStep.icon.source;
+ iconClassname =
+ theme.palette.mode === 'dark'
+ ? `icon-${iconName}-white`
+ : `icon-${iconName}-black`;
+ } else if (configStep.icon.type === 'url') {
+ // For URL icons, we'll use the URL directly in a special way
+ iconClassname = `custom-url:${configStep.icon.source}`;
+ }
+ }
+ // No fallback - leave iconClassname empty if no icon is configured
+
+ return {
+ id: configStep.id,
+ text: configStep.text,
+ iconClassname,
+ };
+ });
+ }, [config.steps, theme.palette.mode]);
+
+ // Don't render if disabled or no steps configured
+ if (!config.enabled || steps.length === 0) {
+ return null;
+ }
+
+ const title = t('page.importEntitiesSubtitle');
+
+ return (
+
+
setExpanded(expanded)}
+ >
+ }
+ id="add-repository-summary"
+ >
+
+ {title}
+
+
+
+
+ {steps.map(step => (
+
+
+
+ ))}
+
+
+
+
+ );
+};
diff --git a/workspaces/bulk-import/plugins/bulk-import/src/hooks/index.ts b/workspaces/bulk-import/plugins/bulk-import/src/hooks/index.ts
index 39a4af9da9..8e2a1d37ea 100644
--- a/workspaces/bulk-import/plugins/bulk-import/src/hooks/index.ts
+++ b/workspaces/bulk-import/plugins/bulk-import/src/hooks/index.ts
@@ -15,6 +15,8 @@
*/
export { useAddedRepositories } from './useAddedRepositories';
+export { useInstructionsConfig } from './useInstructionsConfig';
+export { useInstructionsPreference } from './useInstructionsPreference';
export { useRepositories } from './useRepositories';
export {
useNumberOfApprovalTools,
diff --git a/workspaces/bulk-import/plugins/bulk-import/src/hooks/useInstructionsConfig.ts b/workspaces/bulk-import/plugins/bulk-import/src/hooks/useInstructionsConfig.ts
new file mode 100644
index 0000000000..d931622e93
--- /dev/null
+++ b/workspaces/bulk-import/plugins/bulk-import/src/hooks/useInstructionsConfig.ts
@@ -0,0 +1,119 @@
+/*
+ * Copyright Red Hat, Inc.
+ *
+ * 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 { configApiRef, useApi } from '@backstage/core-plugin-api';
+
+export interface InstructionsStep {
+ id: string;
+ text: string;
+ icon?: {
+ type: 'builtin' | 'url';
+ source: string;
+ };
+}
+
+export interface InstructionsConfig {
+ enabled: boolean;
+ defaultExpanded: boolean;
+ steps: InstructionsStep[];
+}
+
+/**
+ * Hook to get instructions configuration from app-config.yaml
+ */
+export function useInstructionsConfig(): InstructionsConfig {
+ const configApi = useApi(configApiRef);
+ const bulkImportConfig = configApi.getOptionalConfig('bulkImport');
+ const instructionsEnabled =
+ configApi.getOptionalBoolean('bulkImport.instructionsEnabled') ?? true;
+ const instructionsDefaultExpanded =
+ configApi.getOptionalBoolean('bulkImport.instructionsDefaultExpanded') ??
+ true;
+ let instructionsSteps =
+ configApi.getOptionalConfigArray('bulkImport.instructionsSteps') ?? [];
+
+ if (instructionsSteps.length === 0 && bulkImportConfig) {
+ instructionsSteps =
+ bulkImportConfig.getOptionalConfigArray('instructionsSteps') ?? [];
+
+ // If still empty, try accessing the raw data (bypass schema validation)
+ if (instructionsSteps.length === 0) {
+ try {
+ const rawData = (bulkImportConfig as any).data;
+ if (
+ rawData &&
+ rawData.instructionsSteps &&
+ Array.isArray(rawData.instructionsSteps)
+ ) {
+ // Convert raw data to InstructionsStep format
+ instructionsSteps = rawData.instructionsSteps.map((step: any) => ({
+ id: step.id,
+ text: step.text,
+ icon: step.icon
+ ? {
+ type: step.icon.type as 'builtin' | 'url',
+ source: step.icon.source,
+ }
+ : undefined,
+ }));
+ }
+ } catch (error) {
+ // Silently handle config access errors
+ }
+ }
+ }
+
+ // Use the flat config values
+ const enabled = instructionsEnabled;
+ const defaultExpanded = instructionsDefaultExpanded;
+ let stepsConfig = instructionsSteps;
+
+ // Legacy fallback approach (no longer needed with current config structure)
+ if (stepsConfig.length === 0) {
+ // Try accessing via bulkImport config object
+ const legacyBulkImportConfig = configApi.getOptionalConfig('bulkImport');
+ if (legacyBulkImportConfig) {
+ const instructionsConfig =
+ legacyBulkImportConfig.getOptionalConfig('instructions');
+ if (instructionsConfig) {
+ stepsConfig = instructionsConfig.getOptionalConfigArray('steps') ?? [];
+ }
+ }
+ }
+
+ const steps: InstructionsStep[] =
+ stepsConfig.length > 0
+ ? stepsConfig.map(stepConfig => ({
+ id: stepConfig.getString('id'),
+ text: stepConfig.getString('text'),
+ icon: stepConfig.has('icon')
+ ? {
+ type: stepConfig.getString('icon.type') as 'builtin' | 'url',
+ source: stepConfig.getString('icon.source'),
+ }
+ : undefined,
+ }))
+ : []; // No fallback needed - config should work now
+
+ // Use config steps directly - no hardcoded fallback
+ const finalSteps = steps;
+
+ return {
+ enabled,
+ defaultExpanded,
+ steps: finalSteps,
+ };
+}
diff --git a/workspaces/bulk-import/plugins/bulk-import/src/hooks/useInstructionsPreference.ts b/workspaces/bulk-import/plugins/bulk-import/src/hooks/useInstructionsPreference.ts
new file mode 100644
index 0000000000..dd9bdbf235
--- /dev/null
+++ b/workspaces/bulk-import/plugins/bulk-import/src/hooks/useInstructionsPreference.ts
@@ -0,0 +1,62 @@
+/*
+ * Copyright Red Hat, Inc.
+ *
+ * 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 { useCallback, useEffect, useState } from 'react';
+
+const STORAGE_KEY = 'bulk-import-instructions-expanded';
+
+/**
+ * Hook to manage user preference for instructions section expanded state
+ * Persists the state in localStorage
+ */
+export function useInstructionsPreference(
+ defaultExpanded: boolean,
+): [boolean, (expanded: boolean) => void] {
+ const [isExpanded, setIsExpanded] = useState(() => {
+ try {
+ const stored = localStorage.getItem(STORAGE_KEY);
+ return stored !== null ? JSON.parse(stored) : defaultExpanded;
+ } catch {
+ // If there's an error reading from localStorage, use the default
+ return defaultExpanded;
+ }
+ });
+
+ const setExpanded = useCallback((expanded: boolean) => {
+ setIsExpanded(expanded);
+ try {
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(expanded));
+ } catch {
+ // Silently fail if localStorage is not available
+ // The state will still work for the current session
+ }
+ }, []);
+
+ // Update state if defaultExpanded changes and no user preference is stored
+ useEffect(() => {
+ try {
+ const stored = localStorage.getItem(STORAGE_KEY);
+ if (stored === null) {
+ setIsExpanded(defaultExpanded);
+ }
+ } catch {
+ // If localStorage is not available, just use the default
+ setIsExpanded(defaultExpanded);
+ }
+ }, [defaultExpanded]);
+
+ return [isExpanded, setExpanded];
+}