Move certificate transforms into header dropdown
This commit is contained in:
@@ -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
@@ -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();
|
||||||
|
|||||||
@@ -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(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user