Compare commits

..
1 Commits
Author SHA1 Message Date
KCA 8717c7021e Move certificate export and revoke actions into header 2026-10-27 00:48:03 +08:00
3 changed files with 25 additions and 65 deletions

No files matched your search

+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
text is available on export.
- Certificate details are split into Overview, Extensions, Validation,
Metadata and CA tabs, with hash deep links. Header actions provide export,
renewal, revocation, deletion and certificate transformation. They show
Metadata, Transform and CA tabs, with hash deep links. Header actions provide
export, renewal, revocation and deletion. They show
subject/issuer, serial, validity, signatures, MD5/SHA-1/SHA-256
fingerprints, parsed extensions, OpenSSL validation errors, purposes and the
issuer chain.
+1 -28
View File
@@ -101,13 +101,11 @@ test("core PKI workflow", async ({ page }) => {
await page.getByRole("button", { name: "Search" }).click();
await expect(page.getByRole("link", { name: leafName, exact: true })).toBeVisible();
// Certificate header controls replace the Export, Revoke and Transform tabs.
// Certificate header actions replace the Export and Revoke tabs.
await page.getByRole("link", { name: leafName, exact: true }).click();
const leafUrl = page.url();
const certificateTabs = page.locator(".nav-tabs");
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: "Transform", exact: true })).toHaveCount(0);
const downloadPromise = page.waitForEvent("download");
await page.getByRole("button", { name: "Export", exact: true }).click();
@@ -119,31 +117,6 @@ test("core PKI workflow", async ({ page }) => {
expect(download.suggestedFilename()).toBe(`${leafName}.crt`);
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 expect(page.getByRole("menuitem", { name: "Renew", exact: true })).toBeVisible();
await expect(page.getByRole("menuitem", { name: "Delete", exact: true })).toBeVisible();
+22 -35
View File
@@ -45,7 +45,6 @@ pub fn CertificateDetailPage(vault_id: String, cert_id: String) -> View {
let info = create_signal(None::<String>);
let openssl_config = create_signal(String::new());
let actions_open = create_signal(false);
let transform_open = create_signal(false);
let export_open = create_signal(false);
let revoke_open = create_signal(false);
let can_revoke = create_signal(false);
@@ -353,16 +352,12 @@ pub fn CertificateDetailPage(vault_id: String, cert_id: String) -> View {
a(class="btn btn-outline-light", href=format!("/vaults/{id}/certificates")) { "Back" }
button(class="btn btn-outline-light", r#type="button", on:click=move |_| {
actions_open.set(false);
transform_open.set(false);
export_open.set(true);
}) { "Export" }
div(class="dropdown") {
button(class="btn btn-primary dropdown-toggle", r#type="button", aria-haspopup="true",
aria-expanded=if actions_open.get() { "true" } else { "false" },
on:click=move |_| {
transform_open.set(false);
actions_open.set(!actions_open.get());
}) { "Actions" }
on:click=move |_| actions_open.set(!actions_open.get())) { "Actions" }
(if actions_open.get() {
view! {
div(class="dropdown-menu dropdown-menu-end show", role="menu") {
@@ -375,34 +370,6 @@ pub fn CertificateDetailPage(vault_id: String, cert_id: String) -> 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") {
@@ -424,6 +391,7 @@ pub fn CertificateDetailPage(vault_id: String, cert_id: String) -> View {
(tab("extensions", "Extensions"))
(tab("validation", "Validation"))
(tab("metadata", "Metadata"))
(tab("transform", "Transform"))
(if is_ca { tab("ca", "CA") } else { view! {} })
}
(if active_tab.get_clone() == "overview" {
@@ -504,6 +472,23 @@ pub fn CertificateDetailPage(vault_id: String, cert_id: String) -> 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" {
view! {
div(id="panel-ca", role="tabpanel", aria-labelledby="tab-ca", class="card mt-3") {
@@ -688,7 +673,9 @@ fn initial_tab() -> String {
.unwrap_or_default();
let hash = hash.trim_start_matches('#');
match hash {
"overview" | "extensions" | "validation" | "metadata" | "ca" => hash.to_string(),
"overview" | "extensions" | "validation" | "metadata" | "transform" | "ca" => {
hash.to_string()
}
_ => "overview".to_string(),
}
}