diff --git a/static/js/publisher/components/SectionNav/SectionNav.tsx b/static/js/publisher/components/SectionNav/SectionNav.tsx index 9e1d7f0165..3e5f2eff5e 100644 --- a/static/js/publisher/components/SectionNav/SectionNav.tsx +++ b/static/js/publisher/components/SectionNav/SectionNav.tsx @@ -1,55 +1,66 @@ import { Link } from "react-router-dom"; import { Tabs } from "@canonical/react-components"; +import { usePublisher } from "../../hooks"; + type Props = { activeTab: string; snapName: string | undefined; }; function SectionNav({ activeTab, snapName }: Props): React.JSX.Element { - return ( - - ); + const { data: publisherData } = usePublisher(); + + const links = [ + { + label: "Listing", + active: activeTab === "listing" || !activeTab, + to: `/${snapName}/listing`, + component: Link, + }, + { + label: "Builds", + active: activeTab === "builds", + to: `/${snapName}/builds`, + component: Link, + }, + { + label: "Releases", + active: activeTab === "releases", + to: `/${snapName}/releases`, + component: Link, + }, + { + label: "Metrics", + active: activeTab === "metrics", + to: `/${snapName}/metrics`, + component: Link, + }, + { + label: "Publicise", + active: activeTab === "publicise", + to: `/${snapName}/publicise`, + component: Link, + }, + { + label: "Settings", + active: activeTab === "settings", + to: `/${snapName}/settings`, + component: Link, + }, + ]; + + // TODO: verify if the snap has CVE data before adding the link + if (publisherData?.publisher?.is_canonical) { + links.splice(3, 0, { + label: "Vulnerabilities", + active: activeTab === "vulnerabilities", + to: `/${snapName}/cves`, + component: Link, + }); + } + + return ; } export default SectionNav; diff --git a/static/js/publisher/hooks/useCves.ts b/static/js/publisher/hooks/useCves.ts new file mode 100644 index 0000000000..1b3eea905c --- /dev/null +++ b/static/js/publisher/hooks/useCves.ts @@ -0,0 +1,20 @@ +import { useQuery } from "react-query"; + +function useCves() { + return useQuery({ + queryKey: ["cves"], + queryFn: async () => { + const response = await fetch(`/api/docker/3213/cves`); + + if (!response.ok) { + throw new Error("Unable to fetch CVEs"); + } + + const responseData = await response.json(); + + return responseData; + }, + }); +} + +export default useCves; diff --git a/static/js/publisher/pages/SnapCves/SnapCves.tsx b/static/js/publisher/pages/SnapCves/SnapCves.tsx new file mode 100644 index 0000000000..4b8de2f870 --- /dev/null +++ b/static/js/publisher/pages/SnapCves/SnapCves.tsx @@ -0,0 +1,295 @@ +import { useParams } from "react-router-dom"; + +// import { setPageTitle } from "../../utils"; +import { useQuery } from "react-query"; +import { MainTable, Strip, Select, Form } from "@canonical/react-components"; +import { Dispatch, SetStateAction, useEffect, useState } from "react"; +import SectionNav from "../../components/SectionNav"; + +interface ICve { + id: string; + status: string; + description: string; + cvss_score: number | null; + cvss_severity: string | null; + ubuntu_priority: string | null; + // affected binary + name: string; + version: string; + fixed_version: string | null; +} + +const SnapCveIdCell = ({ + id, + isExpanded, + setExpandedRow, + index, + hasDescription = false, +}: { + id: string; + isExpanded: boolean; + setExpandedRow: Dispatch>; + index: number; + hasDescription?: boolean; +}): JSX.Element => { + let link: string = ""; + + if (id.startsWith("http")) { + link = id; + } else if (id.startsWith("CVE")) { + link = `https://ubuntu.com/security/${id}`; + } + + return ( + <> + + {link ? ( + + {id} + + ) : ( + {id} + )} + + ); +}; + +const SnapCveSeverityCell = ({ severity }: { severity: string | null }) => { + if (!severity) { + return ( + <> + unknown + + ); + } + + const severityIcon = `p-icon--${severity}-priority`; + + return ( + <> + {severity} + + ); +}; + +const SnapCveStatusCell = ({ + status, + fixedVersion, +}: { + status: string; + fixedVersion: string | null; +}) => { + const statusIcon = status === "fixed" ? `p-icon--success` : `p-icon--error`; + const statusLabel = status === "fixed" ? "fixed" : "vulnerable"; + return ( + <> + {statusLabel} + {fixedVersion && ( + <> + {" "} + + {fixedVersion} + + + )} + + ); +}; + +function SnapCves(): JSX.Element { + const { snapId } = useParams(); + const [currentRevision, setCurrentRevision] = useState(null); + const { data: revisionsData, isLoading: isRevisionsLoading } = useQuery({ + queryKey: ["snapRevisions", snapId], + queryFn: async () => { + const response = await fetch(`/api/${snapId}/cves`); + if (!response.ok) { + throw new Error("There was a problem fetching listing data"); + } + const data = await response.json(); + if (!data.success) { + throw new Error(data.message); + } + + return data.data; + }, + staleTime: 1000 * 60 * 60 * 12, // 12 hours + cacheTime: 1000 * 60 * 60 * 12, // 12 hours + refetchOnWindowFocus: false, + }); + + useEffect(() => { + if (!currentRevision && revisionsData?.revisions?.length) { + setCurrentRevision(revisionsData.revisions[0]); + } + }, [currentRevision, revisionsData]); + + const { data, isLoading: isCvesLoading } = useQuery({ + queryKey: ["cves", snapId, currentRevision], + queryFn: async () => { + const response = await fetch(`/api/${snapId}/${currentRevision}/cves`); + + if (!response.ok) { + throw new Error("There was a problem fetching listing data"); + } + const data = await response.json(); + + if (!data.success) { + throw new Error(data.message); + } + + return data.data; + }, + enabled: !!currentRevision, + staleTime: 1000 * 60 * 60 * 12, // 12 hours + cacheTime: 1000 * 60 * 60 * 12, // 12 hours + refetchOnWindowFocus: false, + }); + + const [expandedRow, setExpandedRow] = useState(null); + + const headers = [ + { + content: "CVE ID", + width: "20%", + style: { flexBasis: "20%" }, + }, + { + content: "Severity", + width: "10%", + style: { flexBasis: "10%" }, + }, + { content: "Status", width: "25%", style: { flexBasis: "25%" } }, + { + content: "Revision", + width: "10%", + style: { flexBasis: "10%" }, + }, + { + content: "Affected source", + width: "15%", + style: { flexBasis: "15%" }, + }, + { + content: "Source version", + width: "20%", + style: { flexBasis: "20%" }, + }, + ]; + + const getData = () => { + return data.map((cve: ICve, index: number) => { + const columns = [ + { + content: ( + + ), + className: "u-truncate", + }, + { content: }, + { + content: ( + + ), + }, + { content: currentRevision }, + { content: cve.name }, + { content: cve.version }, + ] as { content: React.ReactNode; style?: React.CSSProperties }[]; + columns.forEach((column, i) => { + column.style = headers[i].style; + }); + + return { + columns, + expanded: expandedRow === index, + expandedContent:

{cve.description}

, + }; + }); + }; + + console.table(data); + // setPageTitle(`Listing data for ${snapId}`); + + const isLoading = isCvesLoading || isRevisionsLoading; + + const revisionSelectOptions = revisionsData?.revisions.map( + (revision: string) => ({ + label: revision, + value: revision, + }) + ); + + return ( + <> +

+ My snaps / {snapId} / + CVEs +

+ + {isLoading && ( + +

+  Loading{" "} + {snapId} CVE data +

+
+ )} + + {data && ( + <> + + +
+