/* * Copyright (C) 2023 Red Hat, Inc. * SPDX-License-Identifier: LGPL-2.1-or-later */ import cockpit from "cockpit"; import * as python from "python.js"; import StackTrace from "stacktrace-js"; import { fmt_to_fragments as fmtToFragments } from "utils"; import React, { cloneElement, useContext, useEffect, useState } from "react"; import { Alert } from "@patternfly/react-core/dist/esm/components/Alert/index.js"; import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js"; import { Checkbox } from "@patternfly/react-core/dist/esm/components/Checkbox/index.js"; import { Content, ContentVariants } from "@patternfly/react-core/dist/esm/components/Content/index.js"; import { Divider } from "@patternfly/react-core/dist/esm/components/Divider/index.js"; import { Form, FormGroup } from "@patternfly/react-core/dist/esm/components/Form/index.js"; import { HelperText, HelperTextItem } from "@patternfly/react-core/dist/esm/components/HelperText/index.js"; import { InputGroup, InputGroupItem } from "@patternfly/react-core/dist/esm/components/InputGroup/index.js"; import { Modal, ModalBody, ModalFooter, ModalHeader, ModalVariant } from "@patternfly/react-core/dist/esm/components/Modal/index.js"; import { Tab, Tabs, TabTitleText } from "@patternfly/react-core/dist/esm/components/Tabs/index.js"; import { TextArea } from "@patternfly/react-core/dist/esm/components/TextArea/index.js"; import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput/index.js"; import { ValidatedOptions } from "@patternfly/react-core/dist/esm/helpers/constants.js"; import { DisconnectedIcon } from "@patternfly/react-icons/dist/esm/icons/disconnected-icon"; import { ExternalLinkAltIcon } from "@patternfly/react-icons/dist/esm/icons/external-link-alt-icon"; import { EyeIcon } from "@patternfly/react-icons/dist/esm/icons/eye-icon"; import { EyeSlashIcon } from "@patternfly/react-icons/dist/esm/icons/eye-slash-icon"; import { BUGZILLA_BASE_URL, buildBugDescription, buildBugSummary, createBugzillaEnterBug } from "../helpers/bugzilla.js"; import { exitGui } from "../helpers/exit.js"; import { convertToExtlinkIfNeeded } from "../helpers/extlink.js"; import { debug, error } from "../helpers/log.js"; import { AppVersionContext, NetworkContext, OsReleaseContext, SystemTypeContext } from "../contexts/Common.jsx"; import createBugzillaBug from "../scripts/create-bugzilla-bug.py"; import searchBugzillaBugs from "../scripts/search-bugzilla-bugs.py"; import validateBugzillaApiKeyScript from "../scripts/validate-bugzilla-api-key.py"; import { ExternalLink } from "./common/ExternalLink.jsx"; import "./Error.scss"; const _ = cockpit.gettext; // When "Restrict access to the report" is checked, the bug is restricted to this group (GTK uses the same default). const DEFAULT_RESTRICT_GROUP = "fedora_contrib_private"; const JOURNAL_LOG = "/tmp/journal.log"; const ANACONDA_LOG = "/tmp/anaconda.log"; const STORAGE_LOG = "/tmp/storage.log"; const PROGRAM_LOG = "/tmp/program.log"; const PACKAGING_LOG = "/tmp/packaging.log"; const WEBUI_LOG = "/tmp/anaconda-webui.log"; const LOG_FILES = [ JOURNAL_LOG, ANACONDA_LOG, STORAGE_LOG, PROGRAM_LOG, PACKAGING_LOG, WEBUI_LOG ]; const ensureMaximumReportURLLength = (reportURL) => { const newUrl = new URL(reportURL); // The current limit on URL length is 8KiB server limit. const searchParamsLimits = [ // Summary should be short { length: 256, param: "short_desc" }, // We reserve some space in Details text for attachment message which // will be always appended to the end. { length: 8192 - 256 - 100, param: "comment" }, ]; const sp = newUrl.searchParams; searchParamsLimits.forEach((limit) => { if (sp.get(limit.param)?.length > limit.length) { sp.set(limit.param, sp.get(limit.param).slice(0, limit.length)); } }); return newUrl.href; }; const addLogAttachmentCommentToReportURL = (reportURL) => { const newUrl = new URL(reportURL); const logFile = JOURNAL_LOG; const comment = newUrl.searchParams.get("comment") || ""; newUrl.searchParams.set("comment", comment + "\n\n" + cockpit.format(_("Please attach the log file $0 to the issue."), logFile)); return newUrl.href; }; const addExceptionDataToReportURL = (url, exception, bugSummary, bugDescription, environmentInfo) => { const newUrl = new URL(url); if (bugSummary) { newUrl.searchParams.append("short_desc", bugSummary); } const stacktrace = exception?.frontendException?.stack; const description = buildBugDescription({ bugDescription, environmentInfo, stacktrace }); if (description) { newUrl.searchParams.append("comment", description); } return newUrl.href; }; const useBugzillaPrefiledReportURL = (component, exception) => { const { REDHAT_BUGZILLA_PRODUCT: product, REDHAT_BUGZILLA_PRODUCT_VERSION: version, } = useContext(OsReleaseContext); const { systemType } = useContext(SystemTypeContext); const isBootIso = systemType === "BOOT_ISO"; const environmentInfo = useEnvironmentInfo(); const baseUrl = createBugzillaEnterBug({ product, version }, component); let href = baseUrl; if (exception) { const bugSummary = buildBugSummary(exception); const initialDescription = buildBugSummary(exception); href = addExceptionDataToReportURL(href, exception, bugSummary, initialDescription, environmentInfo); href = addLogAttachmentCommentToReportURL(href); href = ensureMaximumReportURLLength(href); } return convertToExtlinkIfNeeded(href, !isBootIso); }; const useEnvironmentInfo = () => { const { PRETTY_NAME: prettyName, } = useContext(OsReleaseContext); const appVersion = useContext(AppVersionContext); return [ "---[ System & Environment Information ]---", `OS: ${prettyName}`, `Anaconda version: ${appVersion.backend}`, `Anaconda UI version: ${appVersion.webui}` ].join("\n"); }; /** * Component for displaying manual Bugzilla reporting link * Used when there's no exception (user-initiated feedback) */ const BZManualReportLink = ({ component }) => { const manualReportLinkURL = useBugzillaPrefiledReportURL(component); return ( {fmtToFragments( _("Report an issue or provide feedback by opening the Bugzilla bug entry form $0."), {_("here")} )} ); }; /** * Component for displaying manual Bugzilla reporting option * Used when there's an exception and user prefers manual reporting */ const BZManualReportOption = ({ component, exception }) => { const manualReportLinkURL = useBugzillaPrefiledReportURL(component, exception); return ( {fmtToFragments( _("Prefer to report manually? $0"), {_("Open Bugzilla bug entry form")} )} ); }; /** * Component for API key entry form (step 1) */ const BZAPIKeyEntryForm = ({ bugzillaApiKey, idPrefix, onApiKeyChange }) => { const isBootIso = useContext(SystemTypeContext).systemType === "BOOT_ISO"; const apiKeyUrl = convertToExtlinkIfNeeded(`${BUGZILLA_BASE_URL}/userprefs.cgi?tab=apikey`, !isBootIso); const [apiKeyHidden, setApiKeyHidden] = useState(true); return ( <> {_("Enter your Bugzilla API key to create a bug report. You will be able to review and edit the bug details in the next step.")} onApiKeyChange(val)} type={apiKeyHidden ? "password" : "text"} /> {fmtToFragments( _("You can generate the API key for your Bugzilla account $0."), ( {_("here")} ) )} ); }; /** * Component for bug report tabs (Description, Stacktrace, Environment) */ const BZReportTabs = ({ bugDescription, exception, idPrefix, onDescriptionChange }) => { const environmentInfo = useEnvironmentInfo(); const frontendStackTrace = exception?.frontendException?.stack; return ( {_("Description")}}>