Move certificate transforms into header dropdown
CI / Build and push docker image (push) Successful in 1m56s
CI / Build and push OCSP image (push) Successful in 33s

This commit is contained in:
Walter Oggioni
2026-10-27 07:39:13 +08:00
committed by woggioni
parent 917e0c1d7c
commit a2457ab1f4
3 changed files with 65 additions and 25 deletions
+2 -2
View File
@@ -97,8 +97,8 @@ explicit/mandatory DN entries, default key type/size/curve, default validity).
extensions, and a server-side **Validate extensions** action. OpenSSL config extensions, and a server-side **Validate extensions** action. OpenSSL config
text is available on export. text is available on export.
- Certificate details are split into Overview, Extensions, Validation, - Certificate details are split into Overview, Extensions, Validation,
Metadata, Transform and CA tabs, with hash deep links. Header actions provide Metadata and CA tabs, with hash deep links. Header actions provide export,
export, renewal, revocation and deletion. They show renewal, revocation, deletion and certificate transformation. They show
subject/issuer, serial, validity, signatures, MD5/SHA-1/SHA-256 subject/issuer, serial, validity, signatures, MD5/SHA-1/SHA-256
fingerprints, parsed extensions, OpenSSL validation errors, purposes and the fingerprints, parsed extensions, OpenSSL validation errors, purposes and the
issuer chain. issuer chain.
+28 -1
View File
@@ -101,11 +101,13 @@ test("core PKI workflow", async ({ page }) => {
await page.getByRole("button", { name: "Search" }).click(); await page.getByRole("button", { name: "Search" }).click();
await expect(page.getByRole("link", { name: leafName, exact: true })).toBeVisible(); await expect(page.getByRole("link", { name: leafName, exact: true })).toBeVisible();
// Certificate header actions replace the Export and Revoke tabs. // Certificate header controls replace the Export, Revoke and Transform tabs.
await page.getByRole("link", { name: leafName, exact: true }).click(); await page.getByRole("link", { name: leafName, exact: true }).click();
const leafUrl = page.url();
const certificateTabs = page.locator(".nav-tabs"); const certificateTabs = page.locator(".nav-tabs");
await expect(certificateTabs.getByRole("tab", { name: "Export", exact: true })).toHaveCount(0); await expect(certificateTabs.getByRole("tab", { name: "Export", exact: true })).toHaveCount(0);
await expect(certificateTabs.getByRole("tab", { name: "Revoke", exact: true })).toHaveCount(0); await expect(certificateTabs.getByRole("tab", { name: "Revoke", exact: true })).toHaveCount(0);
await expect(certificateTabs.getByRole("tab", { name: "Transform", exact: true })).toHaveCount(0);
const downloadPromise = page.waitForEvent("download"); const downloadPromise = page.waitForEvent("download");
await page.getByRole("button", { name: "Export", exact: true }).click(); await page.getByRole("button", { name: "Export", exact: true }).click();
@@ -117,6 +119,31 @@ test("core PKI workflow", async ({ page }) => {
expect(download.suggestedFilename()).toBe(`${leafName}.crt`); expect(download.suggestedFilename()).toBe(`${leafName}.crt`);
await expect(exportDialog).toHaveCount(0); await expect(exportDialog).toHaveCount(0);
const transformButton = page.getByRole("button", { name: "Transform", exact: true });
await transformButton.click();
for (const name of ["Extract public key", "Create request", "Create template", "Similar certificate"]) {
await expect(page.getByRole("menuitem", { name, exact: true })).toBeVisible();
}
await page.getByRole("menuitem", { name: "Extract public key", exact: true }).click();
await expect(page).toHaveURL(/\/keys\/[^/]+$/);
await page.goto(leafUrl);
await transformButton.click();
await page.getByRole("menuitem", { name: "Create request", exact: true }).click();
await expect(page).toHaveURL(/\/requests\/[^/]+$/);
await page.goto(leafUrl);
await transformButton.click();
await page.getByRole("menuitem", { name: "Create template", exact: true }).click();
await expect(page).toHaveURL(/\/templates\/[^/]+$/);
await page.goto(leafUrl);
await transformButton.click();
await page.getByRole("menuitem", { name: "Similar certificate", exact: true }).click();
await expect(page).toHaveURL(`${leafUrl}/similar`);
await expect(page.getByRole("heading", { name: "New certificate" })).toBeVisible();
await page.goto(leafUrl);
await page.getByRole("button", { name: "Actions", exact: true }).click(); await page.getByRole("button", { name: "Actions", exact: true }).click();
await expect(page.getByRole("menuitem", { name: "Renew", exact: true })).toBeVisible(); await expect(page.getByRole("menuitem", { name: "Renew", exact: true })).toBeVisible();
await expect(page.getByRole("menuitem", { name: "Delete", exact: true })).toBeVisible(); await expect(page.getByRole("menuitem", { name: "Delete", exact: true })).toBeVisible();
+35 -22
View File
@@ -45,6 +45,7 @@ pub fn CertificateDetailPage(vault_id: String, cert_id: String) -> View {
let info = create_signal(None::<String>); let info = create_signal(None::<String>);
let openssl_config = create_signal(String::new()); let openssl_config = create_signal(String::new());
let actions_open = create_signal(false); let actions_open = create_signal(false);
let transform_open = create_signal(false);
let export_open = create_signal(false); let export_open = create_signal(false);
let revoke_open = create_signal(false); let revoke_open = create_signal(false);
let can_revoke = create_signal(false); let can_revoke = create_signal(false);
@@ -352,12 +353,16 @@ pub fn CertificateDetailPage(vault_id: String, cert_id: String) -> View {
a(class="btn btn-outline-light", href=format!("/vaults/{id}/certificates")) { "Back" } a(class="btn btn-outline-light", href=format!("/vaults/{id}/certificates")) { "Back" }
button(class="btn btn-outline-light", r#type="button", on:click=move |_| { button(class="btn btn-outline-light", r#type="button", on:click=move |_| {
actions_open.set(false); actions_open.set(false);
transform_open.set(false);
export_open.set(true); export_open.set(true);
}) { "Export" } }) { "Export" }
div(class="dropdown") { div(class="dropdown") {
button(class="btn btn-primary dropdown-toggle", r#type="button", aria-haspopup="true", button(class="btn btn-primary dropdown-toggle", r#type="button", aria-haspopup="true",
aria-expanded=if actions_open.get() { "true" } else { "false" }, aria-expanded=if actions_open.get() { "true" } else { "false" },
on:click=move |_| actions_open.set(!actions_open.get())) { "Actions" } on:click=move |_| {
transform_open.set(false);
actions_open.set(!actions_open.get());
}) { "Actions" }
(if actions_open.get() { (if actions_open.get() {
view! { view! {
div(class="dropdown-menu dropdown-menu-end show", role="menu") { div(class="dropdown-menu dropdown-menu-end show", role="menu") {
@@ -370,6 +375,34 @@ pub fn CertificateDetailPage(vault_id: String, cert_id: String) -> View {
} }
} else { view! {} }) } else { view! {} })
} }
div(class="dropdown") {
button(class="btn btn-outline-light dropdown-toggle", r#type="button", aria-haspopup="true",
aria-expanded=if transform_open.get() { "true" } else { "false" },
on:click=move |_| {
actions_open.set(false);
transform_open.set(!transform_open.get());
}) { "Transform" }
(if transform_open.get() {
view! {
div(class="dropdown-menu dropdown-menu-end show", role="menu") {
button(class="dropdown-item", r#type="button", role="menuitem", on:click=move |_| {
transform_open.set(false);
transform("open-key");
}) { "Extract public key" }
button(class="dropdown-item", r#type="button", role="menuitem", on:click=move |_| {
transform_open.set(false);
transform("request");
}) { "Create request" }
button(class="dropdown-item", r#type="button", role="menuitem", on:click=move |_| {
transform_open.set(false);
transform("template");
}) { "Create template" }
a(class="dropdown-item", href=format!("/vaults/{id}/certificates/{cert_id_value}/similar"), role="menuitem",
on:click=move |_| transform_open.set(false)) { "Similar certificate" }
}
}
} else { view! {} })
}
}, },
)) ))
div(class="card mb-3") { div(class="card mb-3") {
@@ -391,7 +424,6 @@ pub fn CertificateDetailPage(vault_id: String, cert_id: String) -> View {
(tab("extensions", "Extensions")) (tab("extensions", "Extensions"))
(tab("validation", "Validation")) (tab("validation", "Validation"))
(tab("metadata", "Metadata")) (tab("metadata", "Metadata"))
(tab("transform", "Transform"))
(if is_ca { tab("ca", "CA") } else { view! {} }) (if is_ca { tab("ca", "CA") } else { view! {} })
} }
(if active_tab.get_clone() == "overview" { (if active_tab.get_clone() == "overview" {
@@ -472,23 +504,6 @@ pub fn CertificateDetailPage(vault_id: String, cert_id: String) -> View {
} }
} }
} else { view! {} }) } else { view! {} })
(if active_tab.get_clone() == "transform" {
view! {
div(id="panel-transform", role="tabpanel", aria-labelledby="tab-transform", class="row g-3") {
div(class="col-12 col-lg-6") {
div(class="card") {
div(class="card-header") { "Transform" }
div(class="card-body d-grid gap-2") {
button(class="btn btn-outline-light", on:click=move |_| transform("open-key")) { "Extract public key" }
button(class="btn btn-outline-light", on:click=move |_| transform("request")) { "Create request" }
button(class="btn btn-outline-light", on:click=move |_| transform("template")) { "Create template" }
a(class="btn btn-outline-light", href=format!("/vaults/{id}/certificates/{cert_id_value}/similar")) { "Similar certificate" }
}
}
}
}
}
} else { view! {} })
(if is_ca && active_tab.get_clone() == "ca" { (if is_ca && active_tab.get_clone() == "ca" {
view! { view! {
div(id="panel-ca", role="tabpanel", aria-labelledby="tab-ca", class="card mt-3") { div(id="panel-ca", role="tabpanel", aria-labelledby="tab-ca", class="card mt-3") {
@@ -673,9 +688,7 @@ fn initial_tab() -> String {
.unwrap_or_default(); .unwrap_or_default();
let hash = hash.trim_start_matches('#'); let hash = hash.trim_start_matches('#');
match hash { match hash {
"overview" | "extensions" | "validation" | "metadata" | "transform" | "ca" => { "overview" | "extensions" | "validation" | "metadata" | "ca" => hash.to_string(),
hash.to_string()
}
_ => "overview".to_string(), _ => "overview".to_string(),
} }
} }