diff --git a/ui/src/app/certificate_requests/components.tsx b/ui/src/app/certificate_requests/components.tsx index dd24ddf..c9da961 100644 --- a/ui/src/app/certificate_requests/components.tsx +++ b/ui/src/app/certificate_requests/components.tsx @@ -1,33 +1,33 @@ import { Dispatch, SetStateAction, useState, ChangeEvent, useEffect } from "react" import { useMutation, useQueryClient } from "react-query" -import { ConfirmationModalData } from "./row" +// import { ConfirmationModalData } from "./row" import { csrMatchesCertificate, splitBundle, validateBundle } from "../utils" import { postCertToID } from "../queries" import { useCookies } from "react-cookie" import { ConfirmationModal, Button, Input, Textarea, Form, Modal } from "@canonical/react-components"; -interface ConfirmationModalProps { - modalData: ConfirmationModalData - setModalData: Dispatch> -} +// interface ConfirmationModalProps { +// modalData: ConfirmationModalData +// setModalData: Dispatch> +// } -export function CertificateRequestConfirmationModal({ modalData, setModalData }: ConfirmationModalProps) { - const confirmQuery = () => { - modalData?.onMouseDownFunc() - setModalData(null) - } - return ( - setModalData(null)} - > -

{modalData?.warningText}

-
- ) -} +// export function CertificateRequestConfirmationModal({ modalData, setModalData }: ConfirmationModalProps) { +// const confirmQuery = () => { +// modalData?.onMouseDownFunc() +// setModalData(null) +// } +// return ( +// setModalData(null)} +// > +//

{modalData?.warningText}

+//
+// ) +// } interface SubmitCertificateModalProps { id: string diff --git a/ui/src/app/certificate_requests/row.tsx b/ui/src/app/certificate_requests/row.tsx index 5d670d3..d27c7d7 100644 --- a/ui/src/app/certificate_requests/row.tsx +++ b/ui/src/app/certificate_requests/row.tsx @@ -1,211 +1,193 @@ -import { useState, Dispatch, SetStateAction, useEffect, useRef } from "react" -import { UseMutationResult, useMutation, useQueryClient } from "react-query" -import { extractCSR, extractCert, splitBundle } from "../utils" -import { RequiredCSRParams, deleteCSR, rejectCSR, revokeCertificate } from "../queries" -import { CertificateRequestConfirmationModal, SubmitCertificateModal, SuccessNotification } from "./components" -import "./../globals.scss" -import { useCookies } from "react-cookie" +import { useState } from "react"; +import { useMutation, useQueryClient } from "react-query"; +import { extractCSR, extractCert, splitBundle } from "../utils"; +import { deleteCSR, rejectCSR, revokeCertificate } from "../queries"; +import { useCookies } from "react-cookie"; +import { ContextualMenu } from "@canonical/react-components"; +import { SubmitCertificateModal, SuccessNotification } from "./components"; type rowProps = { - id: number, - csr: string, - certificate: string + id: number; + csr: string; + certificate: string; +}; - ActionMenuExpanded: number - setActionMenuExpanded: Dispatch> -} +export default function Row({ id, csr, certificate }: rowProps) { + const [cookies] = useCookies(['user_token']); + const [certificateFormOpen, setCertificateFormOpen] = useState(false); // State for modal visibility + const [successNotification, setSuccessNotification] = useState(null); + const [showCSRContent, setShowCSRContent] = useState(false); + const [showCertContent, setShowCertContent] = useState(false); + + const csrObj = extractCSR(csr); + const certs = splitBundle(certificate); + const clientCertificate = certs?.at(0); + const certObj = clientCertificate ? extractCert(clientCertificate) : null; + + const queryClient = useQueryClient(); -export type ConfirmationModalData = { - onMouseDownFunc: () => void - warningText: string -} | null - -export default function Row({ id, csr, certificate, ActionMenuExpanded, setActionMenuExpanded }: rowProps) { - const [cookies, setCookie, removeCookie] = useCookies(['user_token']); - const red = "rgba(199, 22, 43, 1)" - const green = "rgba(14, 132, 32, 0.35)" - const yellow = "rgba(249, 155, 17, 0.45)" - const [successNotification, setSuccessNotification] = useState(null) - const [detailsMenuOpen, setDetailsMenuOpen] = useState(false) - const [certificateFormOpen, setCertificateFormOpen] = useState(false) - const [confirmationModalData, setConfirmationModalData] = useState(null) - - const csrObj = extractCSR(csr) - const certs = splitBundle(certificate) - const clientCertificate = certs.at(0) - const certObj = clientCertificate ? extractCert(clientCertificate) : null - - const queryClient = useQueryClient() const deleteMutation = useMutation(deleteCSR, { onSuccess: () => queryClient.invalidateQueries('csrs') - }) + }); + const rejectMutation = useMutation(rejectCSR, { onSuccess: () => queryClient.invalidateQueries('csrs') - }) + }); + const revokeMutation = useMutation(revokeCertificate, { onSuccess: () => queryClient.invalidateQueries('csrs') - }) - const mutationFunc = (mutation: UseMutationResult, params: RequiredCSRParams) => { - mutation.mutate(params) - } + }); const handleCopy = () => { - navigator.clipboard.writeText(csr).then(function () { - setSuccessNotification("CSR copied to clipboard") - setTimeout(() => { - setSuccessNotification(null); - }, 2500); - }, function (err) { - console.error('could not copy text: ', err); + navigator.clipboard.writeText(csr).then(() => { + setSuccessNotification("CSR copied to clipboard"); + setTimeout(() => setSuccessNotification(null), 2500); }); - } + }; + const handleDownload = () => { const blob = new Blob([csr], { type: 'text/plain' }); const link = document.createElement('a'); link.href = URL.createObjectURL(blob); - link.download = "csr-" + (csrObj.commonName !== undefined ? csrObj.commonName : id.toString()) + ".pem"; - document.body.appendChild(link); + link.download = `csr-${csrObj.commonName || id}.pem`; link.click(); - document.body.removeChild(link); URL.revokeObjectURL(link.href); }; - const handleReject = () => { - setConfirmationModalData({ - onMouseDownFunc: () => mutationFunc(rejectMutation, { id: id.toString(), authToken: cookies.user_token }), - warningText: "Rejecting a Certificate Request means the CSR will remain in this application, but its status will be moved to rejected and the associated certificate will be deleted if there is any. This action cannot be undone." - }) - } - const handleDelete = () => { - setConfirmationModalData({ - onMouseDownFunc: () => mutationFunc(deleteMutation, { id: id.toString(), authToken: cookies.user_token }), - warningText: "Deleting a Certificate Request means this row will be completely removed from the application. This action cannot be undone." - }) - } - const handleRevoke = () => { - setConfirmationModalData({ - onMouseDownFunc: () => mutationFunc(revokeMutation, { id: id.toString(), authToken: cookies.user_token }), - warningText: "Revoking a Certificate will delete it from the table. This action cannot be undone." - }) - } + + const handleReject = () => rejectMutation.mutate({ id: id.toString(), authToken: cookies.user_token }); + const handleDelete = () => deleteMutation.mutate({ id: id.toString(), authToken: cookies.user_token }); + const handleRevoke = () => revokeMutation.mutate({ id: id.toString(), authToken: cookies.user_token }); + + const getExpiryColor = (notAfter?: string): string => { + if (!notAfter) return 'inherit'; + const expiryDate = new Date(notAfter); + const now = new Date(); + const oneDayInMillis = 24 * 60 * 60 * 1000; + const timeDifference = expiryDate.getTime() - now.getTime(); + + if (timeDifference < 0) return "rgba(199, 22, 43, 1)"; + if (timeDifference < oneDayInMillis) return "rgba(249, 155, 17, 0.45)"; + return "rgba(14, 132, 32, 0.35)"; + }; const getFieldDisplay = (label: string, field: string | undefined, compareField?: string | undefined) => { const isMismatched = compareField !== undefined && compareField !== field; return field ? (

{label}:{" "} - + {field}

) : null; }; - const getExpiryColor = (notAfter?: string): string => { - if (!notAfter) return 'inherit'; - - const expiryDate = new Date(notAfter); - const now = new Date(); - const oneDayInMillis = 24 * 60 * 60 * 1000; - const timeDifference = expiryDate.getTime() - now.getTime(); + const handleCSRContentToggle = () => { + setShowCSRContent(!showCSRContent); + if (!showCSRContent) { + setShowCertContent(false); + } + }; - if (timeDifference < 0) { - return red; - } else if (timeDifference < oneDayInMillis) { - return yellow; - } else { - return green; + const handleCertContentToggle = () => { + setShowCertContent(!showCertContent); + if (!showCertContent) { + setShowCSRContent(false); } }; - return ( - <> - - {id} - - - {csrObj.commonName} - - {certificate == "" ? "outstanding" : (certificate == "rejected" ? "rejected" : "fulfilled")} - {certificate === "" ? "" : (certificate === "rejected" ? "" : certObj?.notAfter)} - - - - {successNotification && } - - - - - {certificate == "rejected" ? - : - } - - - - - {certificate == "rejected" || certificate == "" ? - : - + return { + columns: [ + { content: id.toString() }, + { content: csrObj.commonName || "N/A" }, + { content: certificate === "" ? "outstanding" : (certificate === "rejected" ? "rejected" : "fulfilled") }, + { + content: certificate === "" || certificate === "rejected" ? "" : certObj?.notAfter, + style: { backgroundColor: getExpiryColor(certObj?.notAfter) } + }, + { + content: ( + <> + setCertificateFormOpen(true) }, // Opens modal + { + children: "Revoke Certificate", + disabled: certificate === "rejected" || certificate === "", + onClick: handleRevoke } - - - - - -
-
-
-

Request Details

- {getFieldDisplay("Common Name", csrObj.commonName)} - {getFieldDisplay("Subject Alternative Name DNS", csrObj.sansDns && csrObj.sansDns.length > 0 ? csrObj.sansDns.join(', ') : "")} - {getFieldDisplay("Subject Alternative Name IP addresses", csrObj.sansIp && csrObj.sansIp.length > 0 ? csrObj.sansIp.join(', ') : "")} - {getFieldDisplay("Country", csrObj.country)} - {getFieldDisplay("State or Province", csrObj.stateOrProvince)} - {getFieldDisplay("Locality", csrObj.locality)} - {getFieldDisplay("Organization", csrObj.organization)} - {getFieldDisplay("Organizational Unit", csrObj.OrganizationalUnitName)} - {getFieldDisplay("Email Address", csrObj.emailAddress)} -

Certificate request for a certificate authority: {csrObj.is_ca ? "Yes" : "No"}

-
-
- {certObj && (certObj.notBefore || certObj.notAfter || certObj.issuerCommonName) && ( -
-
-

Certificate Details

- {getFieldDisplay("Common Name", certObj.commonName, csrObj.commonName)} - {getFieldDisplay("Subject Alternative Name DNS", certObj.sansDns && certObj.sansDns.join(', '), csrObj.sansDns && csrObj.sansDns.join(', '))} - {getFieldDisplay("Subject Alternative Name IP addresses", certObj.sansIp && certObj.sansIp.join(', '), csrObj.sansIp && csrObj.sansIp.join(', '))} - {getFieldDisplay("Country", certObj.country, csrObj.country)} - {getFieldDisplay("State or Province", certObj.stateOrProvince, csrObj.stateOrProvince)} - {getFieldDisplay("Locality", certObj.locality, csrObj.locality)} - {getFieldDisplay("Organization", certObj.organization, csrObj.organization)} - {getFieldDisplay("Organizational Unit", certObj.OrganizationalUnitName, csrObj.OrganizationalUnitName)} - {getFieldDisplay("Email Address", certObj.emailAddress, csrObj.emailAddress)} - {getFieldDisplay("Start of validity", certObj.notBefore)} - {getFieldDisplay("Expiry Time", certObj.notAfter)} - {getFieldDisplay("Issuer Common Name", certObj.issuerCommonName)} -
-
+ ]} + hasToggleIcon + position="right" + /> + + {certificateFormOpen && ( + )} + + ), + className: "u-align--right has-overflow" + } + ], + expanded: showCSRContent || showCertContent, + expandedContent: ( +
+ {showCSRContent && ( +
+

Certificate Request Content

+ {getFieldDisplay("Common Name", csrObj.commonName)} + {getFieldDisplay("Subject Alternative Name DNS", csrObj.sansDns?.join(', '))} + {getFieldDisplay("Subject Alternative Name IP addresses", csrObj.sansIp?.join(', '))} + {getFieldDisplay("Country", csrObj.country)} + {getFieldDisplay("State or Province", csrObj.stateOrProvince)} + {getFieldDisplay("Locality", csrObj.locality)} + {getFieldDisplay("Organization", csrObj.organization)} + {getFieldDisplay("Organizational Unit", csrObj.OrganizationalUnitName)} + {getFieldDisplay("Email Address", csrObj.emailAddress)} +

Certificate request for a certificate authority: {csrObj.is_ca ? "Yes" : "No"}

- - - {confirmationModalData != null && } - {certificateFormOpen && } - - ) -} \ No newline at end of file + )} + {showCertContent && certObj && ( +
+

Certificate Content

+ {getFieldDisplay("Common Name", certObj.commonName)} + {getFieldDisplay("Subject Alternative Name DNS", certObj.sansDns?.join(', '))} + {getFieldDisplay("Subject Alternative Name IP addresses", certObj.sansIp?.join(', '))} + {getFieldDisplay("Country", certObj.country)} + {getFieldDisplay("State or Province", certObj.stateOrProvince)} + {getFieldDisplay("Locality", certObj.locality)} + {getFieldDisplay("Organization", certObj.organization)} + {getFieldDisplay("Organizational Unit", certObj.OrganizationalUnitName)} + {getFieldDisplay("Email Address", certObj.emailAddress)} + {getFieldDisplay("Start of validity", certObj.notBefore)} + {getFieldDisplay("Expiry Time", certObj.notAfter)} + {getFieldDisplay("Issuer Common Name", certObj.issuerCommonName)} +

Certificate for a certificate authority: {certObj.is_ca ? "Yes" : "No"}

+
+ )} +
+ ) + }; +} diff --git a/ui/src/app/certificate_requests/table.tsx b/ui/src/app/certificate_requests/table.tsx index 33a1d94..5836049 100644 --- a/ui/src/app/certificate_requests/table.tsx +++ b/ui/src/app/certificate_requests/table.tsx @@ -1,99 +1,92 @@ -import { useContext, useState, Dispatch, SetStateAction } from "react" -import { AsideContext } from "../aside" -import Row from "./row" -import { CSREntry } from "../types" +import { useContext, useState, Dispatch, SetStateAction } from "react"; +import { AsideContext } from "../aside"; +import { CSREntry } from "../types"; +import { Button, MainTable } from "@canonical/react-components"; +import Row from "./row"; -function EmptyState({ asideOpen, setAsideOpen }: { asideOpen: boolean, setAsideOpen: Dispatch> }) { +function EmptyState({ asideOpen, setAsideOpen }: { asideOpen: boolean; setAsideOpen: Dispatch> }) { return ( - -
-
-
-

No CSRs available yet.

- -
+
+
+
+

No CSRs available yet.

+
- - ) +
+ ); } type TableProps = { - csrs: CSREntry[] -} + csrs: CSREntry[]; +}; function sortByCSRStatus(a: CSREntry, b: CSREntry) { - const aCSRStatus = a.certificate == "" ? "outstanding" : (a.certificate == "rejected" ? "rejected" : "fulfilled") - const bCSRStatus = b.certificate == "" ? "outstanding" : (b.certificate == "rejected" ? "rejected" : "fulfilled") - if (aCSRStatus < bCSRStatus) { - return -1; - } else if (aCSRStatus > bCSRStatus) { - return 1; - } else { - return 0; - } + const aCSRStatus = a.certificate === "" ? "outstanding" : a.certificate === "rejected" ? "rejected" : "fulfilled"; + const bCSRStatus = b.certificate === "" ? "outstanding" : b.certificate === "rejected" ? "rejected" : "fulfilled"; + return aCSRStatus.localeCompare(bCSRStatus); } function sortByCertStatus(a: CSREntry, b: CSREntry) { - const aCertStatus = (a.certificate == "" ? "" : (a.certificate == "rejected" ? "" : "date")) - const bCertStatus = (b.certificate == "" ? "" : (b.certificate == "rejected" ? "" : "date")) - if (aCertStatus < bCertStatus) { - return -1; - } else if (aCertStatus > bCertStatus) { - return 1; - } else { - return 0; - } + const aCertStatus = a.certificate === "" || a.certificate === "rejected" ? "" : "date"; + const bCertStatus = b.certificate === "" || b.certificate === "rejected" ? "" : "date"; + return aCertStatus.localeCompare(bCertStatus); } -export function CertificateRequestsTable({ csrs: rows }: TableProps) { - const { isOpen: isAsideOpen, setIsOpen: setAsideIsOpen } = useContext(AsideContext) +export function CertificateRequestsTable({ csrs }: TableProps) { + const { isOpen: isAsideOpen, setIsOpen: setAsideIsOpen } = useContext(AsideContext); + + const [actionsMenuExpanded, setActionsMenuExpanded] = useState(0); + const [sortedColumn, setSortedColumn] = useState("none"); + const [sortDescending, setSortDescending] = useState(true); - const [actionsMenuExpanded, setActionsMenuExpanded] = useState(0) - const [sortedColumn, setSortedColumn] = useState('none') - const [sortDescending, setSortDescending] = useState(true) const sortedRows = () => { switch (sortedColumn) { case "csr": - return (sortDescending ? rows.sort(sortByCSRStatus).reverse() : rows.sort(sortByCSRStatus)) + return sortDescending ? csrs.sort(sortByCSRStatus).reverse() : csrs.sort(sortByCSRStatus); case "cert": - return (sortDescending ? rows.sort(sortByCertStatus).reverse() : rows.sort(sortByCertStatus)) + return sortDescending ? csrs.sort(sortByCertStatus).reverse() : csrs.sort(sortByCertStatus); default: - return rows + return csrs; } - } + }; + return (

Certificate Requests

- {rows.length > 0 && } + {csrs.length > 0 && ( + + )}
- - - - - - - - - - - - - { - sortedRows().map((row) => ( - - ) - )} - - {rows.length == 0 && } -
IDDetails { setSortedColumn('csr'); setSortDescending(!sortDescending) }}>CSR Status { setSortedColumn('cert'); setSortDescending(!sortDescending) }}>Certificate Expiry DateActions
+ + Row({ + id: csr.id, + csr: csr.csr, + certificate: csr.certificate, + }) + )} + /> + {csrs.length === 0 && }
- ) -} \ No newline at end of file + ); +} diff --git a/ui/src/app/users/table.tsx b/ui/src/app/users/table.tsx index 1c8e6dd..2e793ca 100644 --- a/ui/src/app/users/table.tsx +++ b/ui/src/app/users/table.tsx @@ -66,7 +66,6 @@ export function UsersTable({ users }: TableProps) { { content: (