{_("If your issue matches one of the bugs above, you can open it and add your information there instead of creating a duplicate report.")}
>
);
};
/**
* Component for bug report details form (step 3)
*/
const BZReportDetailsForm = ({
bugDescription,
bugSummary,
exception,
idPrefix,
onDescriptionChange,
onRestrictAccessChange,
onSummaryChange,
restrictAccessToReport
}) => {
const component = componentFromException(exception);
const isBootIso = useContext(SystemTypeContext).systemType === "BOOT_ISO";
const manualReportLinkURL = useBugzillaPrefiledReportURL(component, exception);
const logFiles = LOG_FILES.map(file => file.replace("/tmp/", ""));
const restrictAccessLearnMoreURL = `${BUGZILLA_BASE_URL}/userprefs.cgi?tab=permissions`;
return (
<>
{_("Bug report details")}
onSummaryChange(val)}
placeholder={_("Brief description of the issue")}
validated={!bugSummary.trim() ? ValidatedOptions.error : ValidatedOptions.default}
/>
{DEFAULT_RESTRICT_GROUP},
(
{_("Learn more about restricted access in the configuration")}
)
)}
onChange={(_event, checked) => onRestrictAccessChange(checked)}
/>
{fmtToFragments(
_("When you submit this bug report, the following log files will be uploaded automatically: $0"),
{logFiles.join(", ")}
)}
{_("These log files may contain sensitive information such as IP addresses, usernames, or other system details.")}
{fmtToFragments(
_("If you do not consent to automatic log file upload, you can $0 instead. Manual reporting allows you to review and selectively attach log files yourself."),
{_("report the issue manually")}
)}
>
);
};
/**
* Helper function to get component name from exception
*/
const componentFromException = (exception) => {
const hasFrontendException = !!exception?.frontendException;
return exception ? (hasFrontendException ? "anaconda-webui" : "anaconda") : "anaconda";
};
/**
* Exception report flow component - handles the 3-step flow for exception reporting
* Step 1: API key entry, Step 2: Duplicate check (skipped if none found), Step 3: Bug details + submit
*/
const BZExceptionReportFlow = ({
buttons,
exception,
idPrefix
}) => {
const isBootIso = useContext(SystemTypeContext).systemType === "BOOT_ISO";
const isConnected = useContext(NetworkContext).connected;
const component = componentFromException(exception);
const {
REDHAT_BUGZILLA_PRODUCT: product,
REDHAT_BUGZILLA_PRODUCT_VERSION: version,
} = useContext(OsReleaseContext);
const environmentInfo = useEnvironmentInfo();
const [isCreatingBug, setIsCreatingBug] = useState(false);
const [bugCreationError, setBugCreationError] = useState(null);
const [bugzillaApiKey, setBugzillaApiKey] = useState("");
const [reportStep, setReportStep] = useState(1); // 1 = API key entry, 2 = duplicate check, 3 = bug report details
const [isValidatingApiKey, setIsValidatingApiKey] = useState(false);
const [duplicateBugs, setDuplicateBugs] = useState([]);
// Note: Summary and description are editable by the user, which is why they're stored in state.
// The stacktrace and environmentInfo are auto-added to the bug report and are not editable.
const [bugSummary, setBugSummary] = useState(buildBugSummary(exception));
const [bugDescription, setBugDescription] = useState(
buildBugSummary(exception)
);
const [restrictAccessToReport, setRestrictAccessToReport] = useState(false);
useEffect(() => {
// Let's make sure we have the latest logs from journal saved to /tmp/journal.log
// Let's not confuse users with syslog
// See https://issues.redhat.com/browse/INSTALLER-4210
cockpit.spawn(["journalctl", "-a"])
.then((output) => (
cockpit.file(JOURNAL_LOG)
.replace(output)
));
}, []);
// Pre-fill bugSummary when exception changes
useEffect(() => {
if (exception) {
const summary = buildBugSummary(exception);
if (summary) {
setBugSummary(summary);
setBugDescription(summary);
}
}
}, [exception]);
const searchDuplicates = async () => {
try {
const searchData = JSON.stringify({
api_key: bugzillaApiKey,
component,
limit: 5,
product,
summary: bugSummary,
});
const searchProcess = python.spawn(searchBugzillaBugs, [], {
environ: ["LC_ALL=C.UTF-8"],
err: "message"
});
searchProcess.input(searchData);
const searchResult = await searchProcess;
const searchResponse = JSON.parse(searchResult);
return searchResponse.bugs || [];
} catch (e) {
debug("Duplicate search failed, continuing without results:", e.message);
return [];
}
};
const handleNext = async () => {
setBugCreationError(null);
setIsValidatingApiKey(true);
try {
const inputData = JSON.stringify({ api_key: bugzillaApiKey });
const process = python.spawn(validateBugzillaApiKeyScript, [], {
environ: ["LC_ALL=C.UTF-8"],
err: "message"
});
process.input(inputData);
await process;
const bugs = await searchDuplicates();
setDuplicateBugs(bugs);
if (bugs.length > 0) {
setReportStep(2);
} else {
setReportStep(3);
}
} catch (e) {
setBugCreationError(e.message);
} finally {
setIsValidatingApiKey(false);
}
};
const createBug = async () => {
setIsCreatingBug(true);
setBugCreationError(null);
try {
// Build complete description using helper function
const stacktrace = exception?.frontendException?.stack;
const description = buildBugDescription({
bugDescription,
environmentInfo,
stacktrace,
});
const bugData = {
api_key: bugzillaApiKey,
component,
description,
log_files: LOG_FILES,
product,
summary: bugSummary,
version,
};
if (restrictAccessToReport) {
bugData.groups = [DEFAULT_RESTRICT_GROUP];
}
const inputJson = JSON.stringify(bugData);
let result;
try {
const process = python.spawn(createBugzillaBug, [], {
environ: ["LC_ALL=C.UTF-8"],
err: "message"
});
process.input(inputJson);
result = await process;
} catch (e) {
setBugCreationError(e.message);
setIsCreatingBug(false);
return;
}
const response = JSON.parse(result);
debug("ANACONDA_BUG_REPORT_ID=" + response.bug_id);
// Success! Open the created bug
const bugUrl = response.url;
const extlinkUrl = convertToExtlinkIfNeeded(bugUrl, !isBootIso);
window.open(extlinkUrl);
setIsCreatingBug(false);
} catch (e) {
// Show error to user
const errorMessage = e.message || String(e);
error("Failed to report issue:", errorMessage);
setBugCreationError(errorMessage);
setIsCreatingBug(false);
}
};
return (
<>
{bugCreationError && (
{bugCreationError}
)}
{reportStep === 1 && (
)}
{reportStep === 2 && (
<>
>
)}
{reportStep === 3 && (
<>
}
onClick={() => { createBug(); return false }}
>
{isCreatingBug ? _("Reporting issue...") : _("Report issue")}
>
)}
{buttons}
>
);
};
/**
* Content component for Bugzilla report modal
* Handles mode selection (network state, user flow, exception flow)
*/
const BZReportContent = ({
buttons,
exception,
idPrefix
}) => {
const isBootIso = useContext(SystemTypeContext).systemType === "BOOT_ISO";
const isConnected = useContext(NetworkContext).connected;
const component = componentFromException(exception);
const networkHelperMessageLive = _("Network not available. Configure the network in the top bar menu to report the issue.");
const networkHelperMessageBootIso = _("Network not available. Configure the network to report the issue.");
if (!isConnected) {
return (
<>
}>
{isBootIso ? networkHelperMessageBootIso : networkHelperMessageLive}
{buttons}
>
);
}
if (!exception) {
return (
<>
{buttons}
>
);
}
return (
);
};
export const BZReportModal = ({
buttons,
exception,
idPrefix,
title,
titleIconVariant
}) => {
const context = exception?.backendException?.contextData?.context || exception?.frontendException?.contextData?.context;
const description = exception
? (
context
? cockpit.format(_("The installer cannot continue due to a critical error: $0"), _(context))
: _("The installer cannot continue due to a critical error.")
)
: undefined;
const header = (
);
return (
{header}
);
};
/**
* Component for displaying exception information
*/
const ExceptionInfo = ({ exception, idPrefix }) => {
const exceptionNamePrefix = exception.backendException?.name ? exception.backendException?.name + ": " : "";
const backendMessage = exception.backendException?.message ? exception.backendException.message : "";
const frontendMessage = exception.frontendException?.message ? exception.frontendException.message : "";
return (
{backendMessage &&
{exceptionNamePrefix + backendMessage}
}
{frontendMessage &&
{exceptionNamePrefix + frontendMessage}
}
);
};
const QuitButton = () => {
const isBootIso = useContext(SystemTypeContext).systemType === "BOOT_ISO";
return (
);
};
const CriticalError = ({ exception }) => {
const idPrefix = "critical-error";
return (
]}
exception={exception}
/>
);
};
const CancelButton = ({ onClose }) => {
return (
);
};
export const UserIssue = ({ setIsReportIssueOpen }) => {
return (
setIsReportIssueOpen(false)} />]}
exception={null}
/>
);
};
/** Build exception state from _error and contextData. Copies message/name explicitly (Error props are non-enumerable). */
const buildExceptionState = async (_error, contextData = {}) => {
const message = _error?.message ?? (typeof _error?.toString === "function" ? _error.toString() : String(_error));
const name = _error?.name;
const hasStack = !!_error?.stack;
if (hasStack) {
let stack;
try {
const arrayStackFrame = await StackTrace.fromError(_error);
stack = arrayStackFrame.map(frame => frame.toString()).join("\n");
} catch (parseError) {
error("StackTrace.fromError failed, using raw stack", parseError);
stack = _error.stack;
}
return {
frontendException: { contextData, message, name, stack },
hasError: true
};
}
return {
backendException: { contextData, message, name },
hasError: true
};
};
export class ErrorBoundary extends React.Component {
constructor (props) {
super(props);
// Allow providing initial exception to the ErrorBoundary constructor in case of a critical error
// that happens before the ErrorBoundary is mounted.
this.state = { backendException: props.backendException, hasError: props.backendException !== undefined };
}
// Add window.onerror and window.onunhandledrejection handlers
componentDidMount () {
const errorHandler = async (_error) => {
const stateUpdate = await buildExceptionState(_error, {});
error("ErrorBoundary caught an error:", stateUpdate.frontendException ?? stateUpdate.backendException);
this.setState(stateUpdate);
return true;
};
window.onerror = async (message, source, lineno, colno, _error) => errorHandler(_error);
window.onunhandledrejection = (event) => errorHandler(event.reason);
}
// React Error Boundary: Catches React rendering errors synchronously
// This prevents errors from propagating and crashing the page before window.onerror fires
static getDerivedStateFromError () {
return { hasError: true };
}
onCritFailBackend = (arg) => {
const { context } = arg || {};
return async (_error) => {
if (this.state.hasError) {
return;
}
const stateUpdate = await buildExceptionState(_error, { context });
error("ErrorBoundary caught an error:", stateUpdate.frontendException ?? stateUpdate.backendException);
this.setState(stateUpdate);
};
};
render () {
if (this.state.hasError) {
return (
);
}
return cloneElement(this.props.children, { onCritFail: this.onCritFailBackend });
}
}