Polish PWA assets, accessible tabs and bulk selection, harden bulk revoke tests
This commit is contained in:
@@ -237,7 +237,7 @@ e2e/ # Playwright browser suite (Node)
|
|||||||
fields plus an advanced JSON editor carrying raw OID/DER and other less
|
fields plus an advanced JSON editor carrying raw OID/DER and other less
|
||||||
common extensions. OpenSSL config remains an export format.
|
common extensions. OpenSSL config remains an export format.
|
||||||
- The PWA service worker is intentionally not registered: certificate and key
|
- The PWA service worker is intentionally not registered: certificate and key
|
||||||
data must never be served stale from a cache. The manifest and icons are
|
data must never be served stale from a cache. The manifest and 192/512
|
||||||
still available for installation.
|
icons remain available for installation.
|
||||||
- Column layouts, window geometry, language packs and other desktop-only XCA
|
- Column layouts, window geometry, language packs and other desktop-only XCA
|
||||||
preferences are not part of the web UI.
|
preferences are not part of the web UI.
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ test("advanced extensions, validation and vault export", async ({ page }) => {
|
|||||||
await expect(page.getByRole("heading", { name: certName })).toBeVisible();
|
await expect(page.getByRole("heading", { name: certName })).toBeVisible();
|
||||||
const certUrl = page.url();
|
const certUrl = page.url();
|
||||||
const certApiUrl = certUrl.replace(/\/vaults\//, "/api/vaults/");
|
const certApiUrl = certUrl.replace(/\/vaults\//, "/api/vaults/");
|
||||||
await page.locator(".nav-tabs").getByRole("button", { name: "Extensions", exact: true }).click();
|
await page.locator(".nav-tabs").getByRole("tab", { name: "Extensions", exact: true }).click();
|
||||||
await expect(page.getByText("CRL Distribution Points", { exact: false })).toBeVisible();
|
await expect(page.getByText("CRL Distribution Points", { exact: false })).toBeVisible();
|
||||||
await expect(page.getByText("1.2.3.4", { exact: false })).toBeVisible();
|
await expect(page.getByText("1.2.3.4", { exact: false })).toBeVisible();
|
||||||
|
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ test("core PKI workflow", async ({ page }) => {
|
|||||||
await page.getByRole("button", { name: "Create certificate" }).click();
|
await page.getByRole("button", { name: "Create certificate" }).click();
|
||||||
await expect(page.getByText("CA certificate", { exact: false })).toBeVisible();
|
await expect(page.getByText("CA certificate", { exact: false })).toBeVisible();
|
||||||
const caUrl = page.url();
|
const caUrl = page.url();
|
||||||
await page.locator(".nav-tabs").getByRole("button", { name: "CA", exact: true }).click();
|
await page.locator(".nav-tabs").getByRole("tab", { name: "CA", exact: true }).click();
|
||||||
await (await field(page, "CA · default signing template")).selectOption("builtin:tls-server");
|
await (await field(page, "CA · default signing template")).selectOption("builtin:tls-server");
|
||||||
await page.getByRole("button", { name: "Save CA" }).click();
|
await page.getByRole("button", { name: "Save CA" }).click();
|
||||||
await expect(page.getByText("CA properties saved.")).toBeVisible();
|
await expect(page.getByText("CA properties saved.")).toBeVisible();
|
||||||
@@ -73,7 +73,7 @@ test("core PKI workflow", async ({ page }) => {
|
|||||||
await page.locator("input[placeholder*='DNS:example.com']").fill(`DNS:${leafName}`);
|
await page.locator("input[placeholder*='DNS:example.com']").fill(`DNS:${leafName}`);
|
||||||
await page.getByRole("button", { name: "Create certificate" }).click();
|
await page.getByRole("button", { name: "Create certificate" }).click();
|
||||||
await expect(page.getByRole("heading", { name: leafName })).toBeVisible();
|
await expect(page.getByRole("heading", { name: leafName })).toBeVisible();
|
||||||
await page.locator(".nav-tabs").getByRole("button", { name: "Extensions", exact: true }).click();
|
await page.locator(".nav-tabs").getByRole("tab", { name: "Extensions", exact: true }).click();
|
||||||
await expect(page.getByText("serverAuth", { exact: false })).toBeVisible();
|
await expect(page.getByText("serverAuth", { exact: false })).toBeVisible();
|
||||||
|
|
||||||
// Search
|
// Search
|
||||||
@@ -84,14 +84,14 @@ test("core PKI workflow", async ({ page }) => {
|
|||||||
|
|
||||||
// Revoke
|
// Revoke
|
||||||
await page.getByRole("link", { name: leafName, exact: true }).click();
|
await page.getByRole("link", { name: leafName, exact: true }).click();
|
||||||
await page.locator(".nav-tabs").getByRole("button", { name: "Revoke", exact: true }).click();
|
await page.locator(".nav-tabs").getByRole("tab", { name: "Revoke", exact: true }).click();
|
||||||
await page.getByRole("button", { name: "Revoke certificate" }).click();
|
await page.getByRole("button", { name: "Revoke certificate" }).click();
|
||||||
await expect(page.getByText("revoked", { exact: true })).toBeVisible();
|
await expect(page.getByText("revoked", { exact: true })).toBeVisible();
|
||||||
|
|
||||||
// CRL from the CA
|
// CRL from the CA
|
||||||
await page.goto(caUrl);
|
await page.goto(caUrl);
|
||||||
await expect(page.getByRole("heading", { name: caName })).toBeVisible();
|
await expect(page.getByRole("heading", { name: caName })).toBeVisible();
|
||||||
await page.locator(".nav-tabs").getByRole("button", { name: "CA", exact: true }).click();
|
await page.locator(".nav-tabs").getByRole("tab", { name: "CA", exact: true }).click();
|
||||||
await page.getByRole("button", { name: "Generate CRL" }).click();
|
await page.getByRole("button", { name: "Generate CRL" }).click();
|
||||||
await expect(page).toHaveURL(/\/crls\//);
|
await expect(page).toHaveURL(/\/crls\//);
|
||||||
await expect(page.getByText(/CRL number \d+ · 1 revoked/)).toBeVisible();
|
await expect(page.getByText(/CRL number \d+ · 1 revoked/)).toBeVisible();
|
||||||
|
|||||||
@@ -379,3 +379,25 @@ class BulkRevocationTests(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert response.json()["errors"][0]["error"] == "invalid certificate id"
|
assert response.json()["errors"][0]["error"] == "invalid certificate id"
|
||||||
|
|
||||||
|
@async_test
|
||||||
|
async def test_bulk_revoke_validation_and_isolation(self) -> None:
|
||||||
|
async with api_client() as client:
|
||||||
|
with oidc_user("alice"):
|
||||||
|
vault_id = await _vault(client)
|
||||||
|
response = await client.post(
|
||||||
|
f"/api/vaults/{vault_id}/certificates/bulk-revoke",
|
||||||
|
json={"certificate_ids": ["00000000-0000-0000-0000-000000000000"], "reason": "superseded", "invalidity_date": "not-a-date"},
|
||||||
|
)
|
||||||
|
assert response.status_code == 400
|
||||||
|
response = await client.post(
|
||||||
|
f"/api/vaults/{vault_id}/certificates/bulk-revoke",
|
||||||
|
json={"certificate_ids": [str(i) for i in range(201)], "reason": "superseded"},
|
||||||
|
)
|
||||||
|
assert response.status_code == 400
|
||||||
|
with oidc_user("bob"):
|
||||||
|
response = await client.post(
|
||||||
|
f"/api/vaults/{vault_id}/certificates/bulk-revoke",
|
||||||
|
json={"certificate_ids": ["00000000-0000-0000-0000-000000000000"], "reason": "superseded"},
|
||||||
|
)
|
||||||
|
assert response.status_code == 404
|
||||||
|
|||||||
@@ -8,12 +8,17 @@
|
|||||||
<meta name="theme-color" content="#0a1720">
|
<meta name="theme-color" content="#0a1720">
|
||||||
<meta name="color-scheme" content="dark">
|
<meta name="color-scheme" content="dark">
|
||||||
<meta name="mobile-web-app-capable" content="yes">
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
<meta name="apple-mobile-web-app-title" content="KCA">
|
<meta name="apple-mobile-web-app-title" content="KCA">
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||||
<link rel="manifest" href="/manifest.webmanifest">
|
<link rel="manifest" href="/manifest.webmanifest">
|
||||||
<link rel="icon" type="image/svg+xml" href="/icons/icon.svg">
|
<link rel="icon" type="image/svg+xml" href="/icons/icon.svg">
|
||||||
|
<link rel="apple-touch-icon" href="/icons/icon-192.png">
|
||||||
<link data-trunk rel="copy-file" href="pwa/manifest.webmanifest" data-target-path=".">
|
<link data-trunk rel="copy-file" href="pwa/manifest.webmanifest" data-target-path=".">
|
||||||
<link data-trunk rel="copy-file" href="pwa/sw.js" data-target-path=".">
|
<link data-trunk rel="copy-file" href="pwa/sw.js" data-target-path=".">
|
||||||
<link data-trunk rel="copy-file" href="pwa/icons/icon.svg" data-target-path="icons">
|
<link data-trunk rel="copy-file" href="pwa/icons/icon.svg" data-target-path="icons">
|
||||||
|
<link data-trunk rel="copy-file" href="pwa/icons/icon-192.png" data-target-path="icons">
|
||||||
|
<link data-trunk rel="copy-file" href="pwa/icons/icon-512.png" data-target-path="icons">
|
||||||
<link data-trunk rel="rust" href="Cargo.toml">
|
<link data-trunk rel="rust" href="Cargo.toml">
|
||||||
<link data-trunk rel="css" href="vendor/bootstrap.min.css">
|
<link data-trunk rel="css" href="vendor/bootstrap.min.css">
|
||||||
<link data-trunk rel="css" href="theme.css">
|
<link data-trunk rel="css" href="theme.css">
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 6.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
@@ -12,6 +12,18 @@
|
|||||||
"src": "/icons/icon.svg",
|
"src": "/icons/icon.svg",
|
||||||
"sizes": "any",
|
"sizes": "any",
|
||||||
"type": "image/svg+xml",
|
"type": "image/svg+xml",
|
||||||
|
"purpose": "any"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/icons/icon-192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/icons/icon-512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png",
|
||||||
"purpose": "any maskable"
|
"purpose": "any maskable"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -276,9 +276,16 @@ pub fn CertificateDetailPage(vault_id: String, cert_id: String) -> View {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let tab = move |key: &'static str, label: &'static str| -> View {
|
let tab = move |key: &'static str, label: &'static str| -> View {
|
||||||
|
let panel_id = format!("panel-{key}");
|
||||||
|
let tab_id = format!("tab-{key}");
|
||||||
view! {
|
view! {
|
||||||
li(class="nav-item") {
|
li(class="nav-item", role="presentation") {
|
||||||
button(class=if active_tab.get_clone() == key { "nav-link active" } else { "nav-link" },
|
button(class=if active_tab.get_clone() == key { "nav-link active" } else { "nav-link" },
|
||||||
|
id=tab_id,
|
||||||
|
r#type="button",
|
||||||
|
role="tab",
|
||||||
|
aria-controls=panel_id,
|
||||||
|
aria-selected=if active_tab.get_clone() == key { "true" } else { "false" },
|
||||||
on:click=move |_| {
|
on:click=move |_| {
|
||||||
active_tab.set(key.to_string());
|
active_tab.set(key.to_string());
|
||||||
if let Some(window) = web_sys::window() {
|
if let Some(window) = web_sys::window() {
|
||||||
@@ -326,7 +333,7 @@ pub fn CertificateDetailPage(vault_id: String, cert_id: String) -> View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ul(class="nav nav-tabs mb-3") {
|
ul(class="nav nav-tabs mb-3", role="tablist", aria-label="Certificate sections") {
|
||||||
(tab("overview", "Overview"))
|
(tab("overview", "Overview"))
|
||||||
(tab("extensions", "Extensions"))
|
(tab("extensions", "Extensions"))
|
||||||
(tab("validation", "Validation"))
|
(tab("validation", "Validation"))
|
||||||
@@ -338,7 +345,7 @@ pub fn CertificateDetailPage(vault_id: String, cert_id: String) -> View {
|
|||||||
}
|
}
|
||||||
(if active_tab.get_clone() == "overview" {
|
(if active_tab.get_clone() == "overview" {
|
||||||
view! {
|
view! {
|
||||||
div(class="row g-3 mb-3") {
|
div(id="panel-overview", role="tabpanel", aria-labelledby="tab-overview", class="row g-3 mb-3") {
|
||||||
div(class="col-12 col-lg-6") {
|
div(class="col-12 col-lg-6") {
|
||||||
div(class="card") {
|
div(class="card") {
|
||||||
div(class="card-header") { "Subject" }
|
div(class="card-header") { "Subject" }
|
||||||
@@ -358,25 +365,27 @@ pub fn CertificateDetailPage(vault_id: String, cert_id: String) -> View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
} else { view! {} }) (if active_tab.get_clone() == "extensions" {
|
} else { view! {} })
|
||||||
|
(if active_tab.get_clone() == "extensions" {
|
||||||
view! {
|
view! {
|
||||||
div(class="card mb-3") {
|
div(id="panel-extensions", role="tabpanel", aria-labelledby="tab-extensions", class="card mb-3") {
|
||||||
div(class="card-header d-flex justify-content-between align-items-center") {
|
div(class="card-header d-flex justify-content-between align-items-center") {
|
||||||
span() { "Extensions" }
|
span() { "Extensions" }
|
||||||
(copy_button(openssl_config.get_clone(), "Copy OpenSSL config"))
|
(copy_button(openssl_config.get_clone(), "Copy OpenSSL config"))
|
||||||
}
|
}
|
||||||
div(class="table-responsive") {
|
div(class="table-responsive") {
|
||||||
table(class="table table-sm ext-table mb-0") {
|
table(class="table table-sm ext-table mb-0") {
|
||||||
thead() { tr() { th() { "Extension" } th() { "Value" } } }
|
thead() { tr() { th(scope="col") { "Extension" } th(scope="col") { "Value" } } }
|
||||||
tbody() { Indexed(list=extensions, view=|ext| view! { tr() { td() { (ext.name) (if ext.critical { view! { span(class="badge badge-critical ms-2") { "critical" } } } else { view! {} }) } td(class="extension-value") { (ext.value) } } }) }
|
tbody() { Indexed(list=extensions, view=|ext| view! { tr() { td() { (ext.name) (if ext.critical { view! { span(class="badge badge-critical ms-2") { "critical" } } } else { view! {} }) } td(class="extension-value") { (ext.value) } } }) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
} else { view! {} }) (if active_tab.get_clone() == "validation" {
|
} else { view! {} })
|
||||||
|
(if active_tab.get_clone() == "validation" {
|
||||||
view! {
|
view! {
|
||||||
div(class="card mb-3") {
|
div(id="panel-validation", role="tabpanel", aria-labelledby="tab-validation", class="card mb-3") {
|
||||||
div(class="card-header") { "Validation" }
|
div(class="card-header") { "Validation" }
|
||||||
div(class="card-body") {
|
div(class="card-body") {
|
||||||
(if validation_errors.get_clone().is_empty() {
|
(if validation_errors.get_clone().is_empty() {
|
||||||
@@ -389,9 +398,10 @@ pub fn CertificateDetailPage(vault_id: String, cert_id: String) -> View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
} else { view! {} }) (if active_tab.get_clone() == "export" {
|
} else { view! {} })
|
||||||
|
(if active_tab.get_clone() == "export" {
|
||||||
view! {
|
view! {
|
||||||
div(class="row g-3") {
|
div(id="panel-export", role="tabpanel", aria-labelledby="tab-export", class="row g-3") {
|
||||||
div(class="col-12 col-lg-6") {
|
div(class="col-12 col-lg-6") {
|
||||||
div(class="card mb-3") {
|
div(class="card mb-3") {
|
||||||
div(class="card-header") { "Export" }
|
div(class="card-header") { "Export" }
|
||||||
@@ -427,7 +437,7 @@ pub fn CertificateDetailPage(vault_id: String, cert_id: String) -> View {
|
|||||||
} else { view! {} })
|
} else { view! {} })
|
||||||
(if active_tab.get_clone() == "metadata" {
|
(if active_tab.get_clone() == "metadata" {
|
||||||
view! {
|
view! {
|
||||||
div(class="row g-3") {
|
div(id="panel-metadata", role="tabpanel", aria-labelledby="tab-metadata", class="row g-3") {
|
||||||
div(class="col-12 col-lg-6") {
|
div(class="col-12 col-lg-6") {
|
||||||
div(class="card") {
|
div(class="card") {
|
||||||
div(class="card-header") { "Metadata" }
|
div(class="card-header") { "Metadata" }
|
||||||
@@ -449,7 +459,7 @@ pub fn CertificateDetailPage(vault_id: String, cert_id: String) -> View {
|
|||||||
} else { view! {} })
|
} else { view! {} })
|
||||||
(if active_tab.get_clone() == "revoke" {
|
(if active_tab.get_clone() == "revoke" {
|
||||||
view! {
|
view! {
|
||||||
div(class="row g-3") {
|
div(id="panel-revoke", role="tabpanel", aria-labelledby="tab-revoke", class="row g-3") {
|
||||||
div(class="col-12 col-lg-6") {
|
div(class="col-12 col-lg-6") {
|
||||||
div(class="card") {
|
div(class="card") {
|
||||||
div(class="card-header") { "Revoke" }
|
div(class="card-header") { "Revoke" }
|
||||||
@@ -464,7 +474,7 @@ pub fn CertificateDetailPage(vault_id: String, cert_id: String) -> View {
|
|||||||
} else { view! {} })
|
} else { view! {} })
|
||||||
(if active_tab.get_clone() == "transform" {
|
(if active_tab.get_clone() == "transform" {
|
||||||
view! {
|
view! {
|
||||||
div(class="row g-3") {
|
div(id="panel-transform", role="tabpanel", aria-labelledby="tab-transform", class="row g-3") {
|
||||||
div(class="col-12 col-lg-6") {
|
div(class="col-12 col-lg-6") {
|
||||||
div(class="card") {
|
div(class="card") {
|
||||||
div(class="card-header") { "Transform" }
|
div(class="card-header") { "Transform" }
|
||||||
@@ -481,7 +491,7 @@ pub fn CertificateDetailPage(vault_id: String, cert_id: String) -> View {
|
|||||||
} else { view! {} })
|
} else { view! {} })
|
||||||
(if is_ca && active_tab.get_clone() == "ca" {
|
(if is_ca && active_tab.get_clone() == "ca" {
|
||||||
view! {
|
view! {
|
||||||
div(class="card mt-3") {
|
div(id="panel-ca", role="tabpanel", aria-labelledby="tab-ca", class="card mt-3") {
|
||||||
div(class="card-header") { "CA operations" }
|
div(class="card-header") { "CA operations" }
|
||||||
div(class="card-body") {
|
div(class="card-body") {
|
||||||
div(class="row g-2 align-items-end mb-3") {
|
div(class="row g-2 align-items-end mb-3") {
|
||||||
@@ -554,7 +564,7 @@ pub fn CertificateDetailPage(vault_id: String, cert_id: String) -> View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
table(class="table table-sm mb-0") {
|
table(class="table table-sm mb-0") {
|
||||||
thead() { tr() { th() { "Serial" } th() { "Revoked" } th() { "Reason" } th() { "Invalid since" } th() {} } }
|
thead() { tr() { th(scope="col") { "Serial" } th(scope="col") { "Revoked" } th(scope="col") { "Reason" } th(scope="col") { "Invalid since" } th(scope="col") {} } }
|
||||||
tbody() { Indexed(list=revocations, view=move |rev| { let rev_id = rev.id.clone(); let rev_for_edit = rev.clone(); view! {
|
tbody() { Indexed(list=revocations, view=move |rev| { let rev_id = rev.id.clone(); let rev_for_edit = rev.clone(); view! {
|
||||||
tr() {
|
tr() {
|
||||||
td(class="fingerprint") { (rev.serial_hex) }
|
td(class="fingerprint") { (rev.serial_hex) }
|
||||||
|
|||||||
@@ -188,14 +188,14 @@ pub fn CertificatesPage(vault_id: String) -> View {
|
|||||||
div(class="card mb-3") {
|
div(class="card mb-3") {
|
||||||
div(class="card-body py-2") {
|
div(class="card-body py-2") {
|
||||||
input(class="form-control", placeholder="Filter by name, subject, issuer, serial or fingerprint…",
|
input(class="form-control", placeholder="Filter by name, subject, issuer, serial or fingerprint…",
|
||||||
bind:value=filter)
|
aria-label="Filter certificates", bind:value=filter)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
div(class="card mb-3") {
|
div(class="card mb-3") {
|
||||||
div(class="card-body") {
|
div(class="card-body") {
|
||||||
div(class="d-flex flex-wrap gap-2 align-items-end") {
|
div(class="d-flex flex-wrap gap-2 align-items-end") {
|
||||||
div(class="me-auto") {
|
div(class="me-auto") {
|
||||||
div(class="small text-secondary") { (format!("{} selected", selected.get_clone().len())) }
|
div(class="small text-secondary", aria-live="polite") { (format!("{} selected", selected.get_clone().len())) }
|
||||||
div(class="d-flex gap-2 mt-1") {
|
div(class="d-flex gap-2 mt-1") {
|
||||||
button(class="btn btn-sm btn-outline-light", on:click=select_all) { "Select all revocable" }
|
button(class="btn btn-sm btn-outline-light", on:click=select_all) { "Select all revocable" }
|
||||||
button(class="btn btn-sm btn-outline-light", on:click=clear_selection) { "Clear" }
|
button(class="btn btn-sm btn-outline-light", on:click=clear_selection) { "Clear" }
|
||||||
@@ -259,7 +259,7 @@ fn flat_html(rows: &[CertificateSummary], vault_id: &str, selected: &[String]) -
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
format!(
|
format!(
|
||||||
"<div class=\"table-responsive\"><table class=\"table table-hover align-middle mb-0\"><thead><tr><th></th><th>Name</th><th>Issuer</th><th>Type</th><th>Serial</th><th>Expires</th><th>Status</th><th>SHA-256</th></tr></thead><tbody>{body}</tbody></table></div>"
|
"<div class=\"table-responsive\"><table class=\"table table-hover align-middle mb-0\"><thead><tr><th scope=\"col\"><span class=\"visually-hidden\">Select</span></th><th scope=\"col\">Name</th><th scope=\"col\">Issuer</th><th scope=\"col\">Type</th><th scope=\"col\">Serial</th><th scope=\"col\">Expires</th><th scope=\"col\">Status</th><th scope=\"col\">SHA-256</th></tr></thead><tbody>{body}</tbody></table></div>"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -333,7 +333,7 @@ fn node_html(
|
|||||||
|
|
||||||
fn checkbox(cert: &CertificateSummary, selected: &[String]) -> String {
|
fn checkbox(cert: &CertificateSummary, selected: &[String]) -> String {
|
||||||
if cert.issuer_id.is_none() {
|
if cert.issuer_id.is_none() {
|
||||||
return "<input class=\"form-check-input\" type=\"checkbox\" disabled title=\"Self-signed certificates cannot be revoked\">".to_string();
|
return "<input class=\"form-check-input\" type=\"checkbox\" disabled aria-label=\"Self-signed certificates cannot be revoked\" title=\"Self-signed certificates cannot be revoked\">".to_string();
|
||||||
}
|
}
|
||||||
let checked = if selected.contains(&cert.id) {
|
let checked = if selected.contains(&cert.id) {
|
||||||
" checked"
|
" checked"
|
||||||
@@ -341,8 +341,9 @@ fn checkbox(cert: &CertificateSummary, selected: &[String]) -> String {
|
|||||||
""
|
""
|
||||||
};
|
};
|
||||||
format!(
|
format!(
|
||||||
"<input class=\"form-check-input\" type=\"checkbox\" data-cert-id=\"{}\"{}>",
|
"<input class=\"form-check-input\" type=\"checkbox\" data-cert-id=\"{}\" aria-label=\"Select {} for revocation\"{}>",
|
||||||
escape(&cert.id),
|
escape(&cert.id),
|
||||||
|
escape(&cert.name),
|
||||||
checked
|
checked
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user