Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
Expand Up @@ -84,7 +84,7 @@ import {
export default function AllUpdatesTab(): JSX.Element {
const navigate = useNavigate();
const { projectId } = useParams<{ projectId: string }>();
const [urlParams, setUrlParams] = useSearchParams();
const [, setUrlParams] = useSearchParams();

const [filter, setFilter] = useState<AllUpdatesTabFilterState>(
ALL_UPDATES_TAB_INITIAL_FILTER,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import {
Box,
Button,
CircularProgress,
Dialog,
DialogActions,
DialogContent,
Expand All @@ -26,8 +27,12 @@ import {
Typography,
} from "@wso2/oxygen-ui";
import { Printer, X } from "@wso2/oxygen-ui-icons-react";
import { useCallback, type JSX } from "react";
import { generateUpdateLevelsReportPdf } from "@features/updates/utils/updateLevelsReportPdf";
import { useCallback, useState, type JSX } from "react";
import {
generateUpdateLevelsReportPdf,
parseBugFixes,
parseDescriptionSections,
} from "@features/updates/utils/updateLevelsReportPdf";
import type { UpdateLevelsReportModalProps, UpdateDescriptionLevel } from "@features/updates/types/updates";

function isInstructionsNonEmpty(text: string | null | undefined): boolean {
Expand All @@ -43,16 +48,25 @@ function formatTimestamp(ts: number): string {
}

function UpdateDescriptionItem({ item }: { item: UpdateDescriptionLevel }): JSX.Element {
const parsed = parseDescriptionSections(item.description);
const displayDesc = parsed.generalDescription || parsed.implementationDetails || parsed.impact;
const bugFixUrls = parseBugFixes(item.bugFixes);
return (
<Box sx={{ pl: 2, py: 0.5, borderLeft: "2px solid", borderColor: "divider" }}>
{item.description?.trim() && (
{displayDesc && (
<Typography variant="body2" color="text.secondary" sx={{ mb: 0.5 }}>
{item.description.trim()}
{displayDesc}
</Typography>
)}
{item.bugFixes?.trim() && (
{bugFixUrls.length > 0 && (
<Typography variant="caption" color="text.secondary">
Bug fixes: {item.bugFixes.trim()}
Bug fixes:{" "}
{bugFixUrls.map((url, i) => (
<span key={i}>
{i > 0 && ", "}
<a href={url} target="_blank" rel="noreferrer">{url}</a>
</span>
))}
</Typography>
)}
Comment thread
dileepapeiris marked this conversation as resolved.
</Box>
Expand All @@ -70,9 +84,15 @@ export default function UpdateLevelsReportModal({
onView,
rawData,
}: UpdateLevelsReportModalProps): JSX.Element | null {
const [isDownloading, setIsDownloading] = useState(false);

const handleDownloadPdf = useCallback(() => {
if (!reportData) return;
generateUpdateLevelsReportPdf(reportData);
setIsDownloading(true);
setTimeout(() => {
generateUpdateLevelsReportPdf(reportData);
setIsDownloading(false);
}, 0);
}, [reportData]);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const handleLevelClick = useCallback(
Expand Down Expand Up @@ -278,91 +298,129 @@ export default function UpdateLevelsReportModal({
<Typography variant="subtitle2" fontWeight={700} sx={{ mb: 2 }}>
Update Details
</Typography>
{reportData.allEntries.map((entry, i) => (
<Box key={i} sx={{ mb: 3 }}>
<Box
sx={{
bgcolor: "warning.main",
color: "warning.contrastText",
px: 2,
py: 1,
borderRadius: 1,
mb: 1,
}}
>
<Typography variant="subtitle2" fontWeight={700}>
{entry.formattedDate} - Update No: {entry.updateNumber}
</Typography>
<Typography variant="caption">
{entry.productName}-{entry.productVersion} ({entry.channel})
</Typography>
</Box>
{entry.description?.trim() && (
<Typography variant="body2" sx={{ mb: 1, pl: 1 }}>
{entry.description.trim()}
</Typography>
)}
{entry.bugFixes?.trim() && (
<Typography variant="body2" sx={{ mb: 1, pl: 1 }}>
<strong>Bug Fixes:</strong> {entry.bugFixes.trim()}
</Typography>
)}
{isInstructionsNonEmpty(entry.instructions) && (
<Box sx={{ mb: 1 }}>
<Typography variant="caption" fontWeight={700} display="block" sx={{ pl: 1, mb: 0.5 }}>
Instructions:
{reportData.allEntries.map((entry, i) => {
const parsedDesc = parseDescriptionSections(entry.description);
const bugFixUrls = parseBugFixes(entry.bugFixes);
return (
<Box key={i} sx={{ mb: 3 }}>
<Box
sx={{
bgcolor: "warning.main",
color: "warning.contrastText",
px: 2,
py: 1,
borderRadius: 1,
mb: 1,
}}
>
<Typography variant="subtitle2" fontWeight={700}>
{entry.formattedDate} - Update No: {entry.updateNumber}
</Typography>
<Box
component="pre"
sx={{
bgcolor: "grey.100",
border: "1px solid",
borderColor: "divider",
borderRadius: 1,
p: 1.5,
m: 0,
fontFamily: "monospace",
fontSize: "0.75rem",
whiteSpace: "pre-wrap",
wordBreak: "break-word",
}}
>
{entry.instructions?.trim()}
</Box>
</Box>
)}
{(entry.securityAdvisories?.length ?? 0) > 0 && (
<Box sx={{ pl: 1 }}>
<Typography
variant="caption"
fontWeight={700}
color="error.main"
display="block"
sx={{ mb: 0.5 }}
>
Security Advisories:
<Typography variant="caption">
{entry.productName}-{entry.productVersion} ({entry.channel})
</Typography>
<Box component="ul" sx={{ pl: 2, m: 0 }}>
{entry.securityAdvisories.map((adv, j) => (
<Box component="li" key={j} sx={{ mb: 0.25 }}>
<Typography variant="caption">
<strong>{adv.id}</strong> ({adv.severity}): {adv.overview}
</Box>
{parsedDesc.generalDescription && (
<Box sx={{ mb: 1, pl: 1 }}>
<Typography variant="caption" fontWeight={700} display="block">
General Description:
</Typography>
<Typography variant="body2">{parsedDesc.generalDescription}</Typography>
</Box>
)}
{parsedDesc.implementationDetails && (
<Box sx={{ mb: 1, pl: 1 }}>
<Typography variant="caption" fontWeight={700} display="block">
Implementation Details:
</Typography>
<Typography variant="body2">{parsedDesc.implementationDetails}</Typography>
</Box>
)}
{parsedDesc.impact && (
<Box sx={{ mb: 1, pl: 1 }}>
<Typography variant="caption" fontWeight={700} display="block">
Impact:
</Typography>
<Typography variant="body2">{parsedDesc.impact}</Typography>
</Box>
)}
{bugFixUrls.length > 0 && (
<Box sx={{ mb: 1, pl: 1 }}>
<Typography variant="caption" fontWeight={700} display="block">
Bug Fixes:
</Typography>
<Box>
{bugFixUrls.map((url, j) => (
<Typography key={j} variant="body2">
<a href={url} target="_blank" rel="noreferrer">{url}</a>
</Typography>
</Box>
))}
))}
</Box>
</Box>
</Box>
)}
</Box>
))}
)}
{isInstructionsNonEmpty(entry.instructions) && (
<Box sx={{ mb: 1 }}>
<Typography variant="caption" fontWeight={700} display="block" sx={{ pl: 1, mb: 0.5 }}>
Instructions:
</Typography>
<Box
component="pre"
sx={{
bgcolor: "grey.100",
border: "1px solid",
borderColor: "divider",
borderRadius: 1,
p: 1.5,
m: 0,
fontFamily: "monospace",
fontSize: "0.75rem",
whiteSpace: "pre-wrap",
wordBreak: "break-word",
}}
>
{entry.instructions?.trim()}
</Box>
</Box>
)}
{(entry.securityAdvisories?.length ?? 0) > 0 && (
<Box sx={{ pl: 1 }}>
<Typography
variant="caption"
fontWeight={700}
color="error.main"
display="block"
sx={{ mb: 0.5 }}
>
Security Advisories:
</Typography>
<Box component="ul" sx={{ pl: 2, m: 0 }}>
{entry.securityAdvisories.map((adv, j) => (
<Box component="li" key={j} sx={{ mb: 0.25 }}>
<Typography variant="caption">
<strong>{adv.id}</strong> ({adv.severity}): {adv.overview}
</Typography>
</Box>
))}
</Box>
</Box>
)}
</Box>
);
})}
</Box>
</DialogContent>
<DialogActions
className="print-hide"
sx={{ "@media print": { display: "none !important" } }}
>
<Button variant="outlined" color="warning" startIcon={<Printer size={18} />} onClick={handleDownloadPdf}>
Download PDF
<Button
variant="outlined"
color="warning"
startIcon={isDownloading ? <CircularProgress size={16} color="inherit" /> : <Printer size={18} />}
onClick={handleDownloadPdf}
disabled={isDownloading}
>
{isDownloading ? "Downloading..." : "Download PDF"}
</Button>
<Button variant="contained" color="warning" onClick={onClose}>
Close
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,70 @@ function isInstructionsNonEmpty(text: string | null | undefined): boolean {
return t !== "" && t !== "n/a" && t !== "na";
}

export type DescriptionSections = {
generalDescription: string;
implementationDetails: string;
impact: string;
};

/**
* Parses a description string that may contain embedded section labels
* ("General Description :", "Implementation details :", "Impact :") and
* returns each section separately. Falls back to treating the whole string
* as the general description when no labels are found.
*/
export function parseDescriptionSections(raw: string | null | undefined): DescriptionSections {
const empty: DescriptionSections = { generalDescription: "", implementationDetails: "", impact: "" };
if (!raw?.trim() || raw.trim().toLowerCase() === "n/a") return empty;

const headerRe = /(?:^|\n)\s*(General\s+Description|Implementation\s+[Dd]etails?|Impact)\s*:/gi;
if (!headerRe.test(raw)) {
return { ...empty, generalDescription: raw.trim() };
}
headerRe.lastIndex = 0;

const hits: Array<{ label: string; headerStart: number; contentStart: number }> = [];
let m: RegExpExecArray | null;
while ((m = headerRe.exec(raw)) !== null) {
hits.push({
label: m[1].replace(/\s+/g, "").toLowerCase(),
headerStart: m.index,
contentStart: m.index + m[0].length,
});
}

const result = { ...empty };
for (let i = 0; i < hits.length; i++) {
const { label, contentStart } = hits[i];
const end = i + 1 < hits.length ? hits[i + 1].headerStart : raw.length;
const content = raw.slice(contentStart, end).trim();
if (label === "generaldescription") result.generalDescription = content;
else if (label.startsWith("implementationdetail")) result.implementationDetails = content;
else if (label === "impact") result.impact = content;
}
return result;
}

/**
* Parses the bugFixes field, which may be a JSON-encoded array of URL strings
* or a plain string, filtering out "N/A" values.
*/
export function parseBugFixes(raw: string | null | undefined): string[] {
if (!raw?.trim()) return [];
try {
const parsed = JSON.parse(raw);
if (Array.isArray(parsed)) {
return parsed
.map((item) => String(item).trim())
.filter((item) => item && item.toLowerCase() !== "n/a");
}
} catch {
// not JSON
}
const t = raw.trim();
return t && t.toLowerCase() !== "n/a" ? [t] : [];
}

/**
* Computes structured report data for the Update Levels Report.
* Used by UpdateLevelsReportModal for in-app display.
Expand Down Expand Up @@ -314,27 +378,13 @@ export function generateUpdateLevelsReportPdf(reportData: UpdateLevelsReportData
doc.text(subText, margin + 3, y + headerLines.length * 6 + 6);
y += boxH + 4;

renderField("General Description", entry.description);
renderField("Implementation Details", entry.bugFixes);
const parsedDesc = parseDescriptionSections(entry.description);
const bugFixUrls = parseBugFixes(entry.bugFixes);

// Impact (aggregated from security advisories)
const advisoriesWithImpact = entry.securityAdvisories?.filter((a) => a.impact?.trim());
if (advisoriesWithImpact && advisoriesWithImpact.length > 0) {
ensureSpace(14);
doc.setFontSize(9);
doc.setFont("helvetica", "bold");
doc.setTextColor(0, 0, 0);
doc.text("Impact:", margin, y);
y += 5;
doc.setFont("helvetica", "normal");
for (const adv of advisoriesWithImpact) {
const impLines = doc.splitTextToSize(adv.impact.trim(), contentW - 4);
ensureSpace(impLines.length * 5 + 3);
doc.text(impLines, margin + 4, y);
y += impLines.length * 5 + 3;
}
y += 2;
}
if (parsedDesc.generalDescription) renderField("General Description", parsedDesc.generalDescription);
if (parsedDesc.implementationDetails) renderField("Implementation Details", parsedDesc.implementationDetails);
if (parsedDesc.impact) renderField("Impact", parsedDesc.impact);
if (bugFixUrls.length > 0) renderField("Bug Fixes", bugFixUrls.join("\n"));

// Instructions
if (isInstructionsNonEmpty(entry.instructions)) {
Expand Down