Skip to content

Commit 116ff9a

Browse files
feat(ui): clavis delete pca modal, added docs with implemented clavis functionality (#837)
* feat(aurora-portal): clavis delete ca mutation with delete-modal Signed-off-by: Vladislav Schur <u.shchur@sap.com> * chore(docs): doc with clavis implemented features * fix(aurora-portal): testing for pca-delete Signed-off-by: Vladislav Schur <u.shchur@sap.com> --------- Signed-off-by: Vladislav Schur <u.shchur@sap.com>
1 parent 6eaafea commit 116ff9a

14 files changed

Lines changed: 417 additions & 37 deletions

File tree

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# Clavis / PCA
2+
3+
This document tracks the Clavis integration in Aurora Portal. The initial backend and UI pieces are implemented, and this note is meant to evolve as the feature grows.
4+
5+
## Current Scope
6+
7+
The feature currently covers project-scoped management of private certificate authorities (PCAs):
8+
9+
- list certificate authorities for a project
10+
- create a certificate authority
11+
- delete a certificate authority
12+
- import a certificate chain for a CA
13+
- list certificates issued by a certificate authority
14+
- fetch certificate authority and certificate details by id
15+
- create certificates under a certificate authority
16+
17+
## Implemented UI
18+
19+
The active UI entry point is the project service route at `/projects/$projectId/services/pca/`.
20+
21+
Implemented screens and interactions:
22+
23+
- PCA list page with loading, error, and empty states
24+
- primary action to create a certificate authority
25+
- row action menu with delete certificate authority
26+
- create modal with FQDN/common name validation
27+
- delete modal with explicit confirmation by typing `delete`
28+
29+
The list page currently renders the CA state, id, and common name. It also shows the translated empty state when no PCAs are available for the current project.
30+
31+
## Implemented BFF
32+
33+
The PCA router is project-scoped and talks to the OpenStack PCA / Clavis service.
34+
35+
| Procedure | OpenStack path | Purpose |
36+
| -------------------- | --------------------------------------------------------------------------------------- | ----------------------------------------------- |
37+
| `list` | `GET /certificate-authorities` | List all certificate authorities in the project |
38+
| `create` | `POST /certificate-authorities` | Create a new certificate authority |
39+
| `getById` | `GET /certificate-authorities/{certificate_authority_id}` | Fetch certificate authority details |
40+
| `delete` | `DELETE /certificate-authorities/{certificate_authority_id}` | Permanently delete a certificate authority |
41+
| `import` | `POST /certificate-authorities/{certificate_authority_id}:importCertificate` | Import the certificate chain for a CA |
42+
| `listCertificates` | `GET /certificate-authorities/{certificate_authority_id}/certificates` | List certificates issued by a CA |
43+
| `createCertificate` | `POST /certificate-authorities/{certificate_authority_id}/certificates` | Create a new certificate under a CA |
44+
| `getByIdCertificate` | `GET /certificate-authorities/{certificate_authority_id}/certificates/{certificate_id}` | Fetch certificate details |
45+
46+
All endpoints expect `project_id` in the request context or input and use the OpenStack service client exposed by the Aurora BFF.
47+
48+
## Data Model Notes
49+
50+
Relevant PCA states are:
51+
52+
- `CREATING`
53+
- `AWAITING_CERTIFICATE`
54+
- `READY`
55+
- `FAILED`
56+
- `UNEXPECTED`
57+
58+
A newly created CA starts in `CREATING`. Once its CSR is generated, it moves to `AWAITING_CERTIFICATE`. Importing the certificate chain transitions it to `READY`, at which point it can issue end-entity certificates.
59+
60+
The CA schema also includes:
61+
62+
- `configuration.subject.common_name`
63+
- `csr`
64+
- `certificate`
65+
- `certificate_chain`
66+
- `imported_certificate_chain`
67+
- `project_id`
68+
69+
## UX and Validation
70+
71+
The create flow currently validates the common name as an FQDN-style value. The delete flow requires a typed confirmation to reduce accidental removal of a CA and its associated certificates.
72+
73+
Error states are surfaced directly in the modal or list view when the BFF call fails.
74+
75+
## Next Areas To Document
76+
77+
The backend already exposes certificate and import operations, but the UI does not yet have dedicated screens for:
78+
79+
- CA detail view
80+
- certificate list view
81+
- certificate detail view
82+
- certificate import flow
83+
- list filtering, sorting, and search controls
84+
85+
Those can be documented once the corresponding UI work lands.

apps/aurora-portal/src/client/routes/_auth/projects/$projectId/services/pca/-components/CreateCaModal.test.tsx renamed to apps/aurora-portal/src/client/routes/_auth/projects/$projectId/services/pca/-components/-modals/CreatePcaModal.test.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import userEvent from "@testing-library/user-event"
44
import { I18nProvider } from "@lingui/react"
55
import { i18n } from "@lingui/core"
66
import { PortalProvider } from "@cloudoperators/juno-ui-components"
7-
import { CreateCaModal } from "./CreateCaModal"
7+
import { CreatePcaModal } from "./CreatePcaModal"
88

99
const mockProjectId = "project-123"
1010
const mockMutateAsync = vi.fn().mockResolvedValue({})
@@ -48,12 +48,12 @@ const renderModal = (onClose = vi.fn()) =>
4848
render(
4949
<I18nProvider i18n={i18n}>
5050
<PortalProvider>
51-
<CreateCaModal open={true} onClose={onClose} />
51+
<CreatePcaModal open={true} onClose={onClose} />
5252
</PortalProvider>
5353
</I18nProvider>
5454
)
5555

56-
describe("CreateCaModal", () => {
56+
describe("CreatePcaModal", () => {
5757
beforeEach(async () => {
5858
vi.clearAllMocks()
5959
await act(async () => {

apps/aurora-portal/src/client/routes/_auth/projects/$projectId/services/pca/-components/CreateCaModal.tsx renamed to apps/aurora-portal/src/client/routes/_auth/projects/$projectId/services/pca/-components/-modals/CreatePcaModal.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@ export interface CreateCaModalProps {
1313
const csrRegex = /^(?=.{1,253}$)(?:\*\.)?(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+[A-Za-z]{2,63}$/
1414
const isValidCommonName = (value: string) => csrRegex.test(value)
1515

16-
export const CreateCaModal = ({ open, onClose }: CreateCaModalProps) => {
16+
export const CreatePcaModal = ({ open, onClose }: CreateCaModalProps) => {
1717
const { t } = useLingui()
1818
const projectId = useProjectId()
1919
const utils = trpcReact.useUtils()
2020

21-
const { isPending, ...createCaMutation } = trpcReact.services.pca.create.useMutation({
21+
const { isPending, ...createPcaMutation } = trpcReact.services.pca.create.useMutation({
2222
onSettled: () => utils.services.pca.list.invalidate(),
2323
})
2424

@@ -40,7 +40,7 @@ export const CreateCaModal = ({ open, onClose }: CreateCaModalProps) => {
4040
onSubmit: async ({ value }) => {
4141
if (isPending) return
4242

43-
await createCaMutation.mutateAsync({
43+
await createPcaMutation.mutateAsync({
4444
project_id: projectId,
4545
configuration: {
4646
subject: { common_name: value.common_name },
@@ -54,7 +54,7 @@ export const CreateCaModal = ({ open, onClose }: CreateCaModalProps) => {
5454
if (isPending) return
5555

5656
form.reset()
57-
createCaMutation.reset()
57+
createPcaMutation.reset()
5858
onClose()
5959
}
6060

@@ -69,9 +69,9 @@ export const CreateCaModal = ({ open, onClose }: CreateCaModalProps) => {
6969
onConfirm={form.handleSubmit}
7070
disableConfirmButton={isPending}
7171
>
72-
{createCaMutation.error?.message && (
72+
{createPcaMutation.error?.message && (
7373
<Message dismissible={false} variant="error" className="mb-4">
74-
{createCaMutation.error.message}
74+
{createPcaMutation.error.message}
7575
</Message>
7676
)}
7777

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { describe, it, expect, vi, beforeEach } from "vitest"
2+
import { act, render, screen, waitFor } from "@testing-library/react"
3+
import userEvent from "@testing-library/user-event"
4+
import { I18nProvider } from "@lingui/react"
5+
import { i18n } from "@lingui/core"
6+
import { PortalProvider } from "@cloudoperators/juno-ui-components"
7+
import type { ComponentProps } from "react"
8+
import type { CertificateAuthority } from "@/server/Services/types/pca"
9+
import { DeletePcaModal } from "./DeletePcaModal"
10+
11+
const mockProjectId = "project-123"
12+
const mockMutateAsync = vi.fn().mockResolvedValue(undefined)
13+
const mockInvalidate = vi.fn()
14+
15+
vi.mock("@/client/hooks", () => ({
16+
useProjectId: () => mockProjectId,
17+
}))
18+
19+
vi.mock("@/client/trpcClient", () => ({
20+
trpcReact: {
21+
useUtils: () => ({
22+
services: {
23+
pca: {
24+
list: {
25+
invalidate: mockInvalidate,
26+
},
27+
},
28+
},
29+
}),
30+
services: {
31+
pca: {
32+
delete: {
33+
useMutation: (options?: { onSettled?: () => void }) => ({
34+
isPending: false,
35+
mutateAsync: async (input: unknown) => {
36+
const result = await mockMutateAsync(input)
37+
options?.onSettled?.()
38+
return result
39+
},
40+
error: null,
41+
}),
42+
},
43+
},
44+
},
45+
},
46+
}))
47+
48+
const mockCa: CertificateAuthority = {
49+
id: "ca-123",
50+
project_id: "project-123",
51+
state: "READY",
52+
}
53+
54+
const renderModal = (overrides: Partial<ComponentProps<typeof DeletePcaModal>> = {}) => {
55+
const props: ComponentProps<typeof DeletePcaModal> = {
56+
pca: mockCa,
57+
open: true,
58+
onClose: vi.fn(),
59+
...overrides,
60+
}
61+
62+
return {
63+
...render(
64+
<I18nProvider i18n={i18n}>
65+
<PortalProvider>
66+
<DeletePcaModal {...props} />
67+
</PortalProvider>
68+
</I18nProvider>
69+
),
70+
props,
71+
}
72+
}
73+
74+
describe("DeletePcaModal", () => {
75+
beforeEach(async () => {
76+
vi.clearAllMocks()
77+
await act(async () => {
78+
i18n.activate("en")
79+
})
80+
})
81+
82+
it("renders and keeps delete action disabled by default", () => {
83+
renderModal()
84+
85+
expect(screen.getByText("Delete certificate authority")).toBeInTheDocument()
86+
expect(screen.getByRole("button", { name: "Delete" })).toBeDisabled()
87+
})
88+
89+
it("enables delete only when user types exact confirmation text", async () => {
90+
const user = userEvent.setup()
91+
renderModal()
92+
93+
const deleteButton = screen.getByRole("button", { name: "Delete" })
94+
const confirmInput = screen.getByPlaceholderText('Type "delete" to confirm')
95+
96+
await user.type(confirmInput, "Delete")
97+
expect(deleteButton).toBeDisabled()
98+
99+
await user.clear(confirmInput)
100+
await user.type(confirmInput, "delete")
101+
expect(deleteButton).toBeEnabled()
102+
})
103+
104+
it("submits delete request with correct ids, invalidates list, and closes modal", async () => {
105+
const user = userEvent.setup()
106+
const onClose = vi.fn()
107+
108+
renderModal({ onClose })
109+
110+
await user.type(screen.getByPlaceholderText('Type "delete" to confirm'), "delete")
111+
await user.click(screen.getByRole("button", { name: "Delete" }))
112+
113+
await waitFor(() => {
114+
expect(mockMutateAsync).toHaveBeenCalledWith({
115+
project_id: mockProjectId,
116+
certificate_authority_id: mockCa.id,
117+
})
118+
expect(mockInvalidate).toHaveBeenCalledTimes(1)
119+
expect(onClose).toHaveBeenCalledTimes(1)
120+
})
121+
})
122+
})

0 commit comments

Comments
 (0)