Skip to content

Commit c42cdad

Browse files
committed
fix: merge conflicts
2 parents 70bc181 + 977e992 commit c42cdad

12 files changed

Lines changed: 361 additions & 22 deletions

File tree

apps/aurora-portal/docs/0011_clavis.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ Implemented screens and interactions:
3131
- certificate list view via `PcaCertificatesListContainer` displays certificates issued by a CA
3232
- certificates list shows CA ID and certificate ID columns with loading, error, and empty states
3333
- disabled "Issue End Entity Certificate" button (placeholder for future issue-certificate task)
34-
- individual certificate rows rendered via `PcaCertificatesTableRow` component
34+
- individual certificate rows rendered via `PcaCertificatesTableRow` component, clicking a row navigates to the certificate detail page
35+
- certificate detail page at `/projects/$projectId/services/pca/$pcaId/$certificateId` shows CA ID, certificate ID, duration/validity, and CSR content with loading, error, and not-found states
3536

3637
The PCA list page renders the CA state, id, and common name with translated empty states when no PCAs are available for the current project.
3738
The certificate list view integrates within the CA details view and fetches certificates via the `listCertificates` endpoint.
@@ -82,9 +83,8 @@ Error states are surfaced directly in the modal or list view when the BFF call f
8283

8384
## Next Areas To Document
8485

85-
The backend already exposes certificate and import operations, but the UI does not yet have dedicated screens for:
86+
The backend already exposes import operations, but the UI does not yet have dedicated screens for:
8687

87-
- certificate detail view
8888
- certificate import flow
8989
- list filtering, sorting, and search controls
9090

apps/aurora-portal/src/client/routeTree.gen.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import { Route as AuthProjectsProjectIdStorageProviderContainersIndexRouteImport
3737
import { Route as AuthProjectsProjectIdServicesPcaPcaIdIndexRouteImport } from "./routes/_auth/projects/$projectId/services/pca/$pcaId/index"
3838
import { Route as AuthProjectsProjectIdNetworkSecuritygroupsSecurityGroupIdIndexRouteImport } from "./routes/_auth/projects/$projectId/network/securitygroups/$securityGroupId/index"
3939
import { Route as AuthProjectsProjectIdNetworkFloatingipsFloatingIpIdIndexRouteImport } from "./routes/_auth/projects/$projectId/network/floatingips/$floatingIpId/index"
40+
import { Route as AuthProjectsProjectIdServicesPcaPcaIdCertificateIdRouteImport } from "./routes/_auth/projects/$projectId/services/pca/$pcaId/$certificateId"
4041
import { Route as AuthProjectsProjectIdStorageProviderContainersContainerNameObjectsIndexRouteImport } from "./routes/_auth/projects/$projectId/storage/$provider/containers/$containerName/objects/index"
4142

4243
const AboutRoute = AboutRouteImport.update({
@@ -201,6 +202,12 @@ const AuthProjectsProjectIdNetworkFloatingipsFloatingIpIdIndexRoute =
201202
path: "/floatingips/$floatingIpId/",
202203
getParentRoute: () => AuthProjectsProjectIdNetworkRoute,
203204
} as any)
205+
const AuthProjectsProjectIdServicesPcaPcaIdCertificateIdRoute =
206+
AuthProjectsProjectIdServicesPcaPcaIdCertificateIdRouteImport.update({
207+
id: "/services/pca/$pcaId/$certificateId",
208+
path: "/services/pca/$pcaId/$certificateId",
209+
getParentRoute: () => AuthProjectsProjectIdRoute,
210+
} as any)
204211
const AuthProjectsProjectIdStorageProviderContainersContainerNameObjectsIndexRoute =
205212
AuthProjectsProjectIdStorageProviderContainersContainerNameObjectsIndexRouteImport.update(
206213
{
@@ -234,6 +241,7 @@ export interface FileRoutesByFullPath {
234241
"/projects/$projectId/network/floatingips/": typeof AuthProjectsProjectIdNetworkFloatingipsIndexRoute
235242
"/projects/$projectId/network/securitygroups/": typeof AuthProjectsProjectIdNetworkSecuritygroupsIndexRoute
236243
"/projects/$projectId/services/pca/": typeof AuthProjectsProjectIdServicesPcaIndexRoute
244+
"/projects/$projectId/services/pca/$pcaId/$certificateId": typeof AuthProjectsProjectIdServicesPcaPcaIdCertificateIdRoute
237245
"/projects/$projectId/network/floatingips/$floatingIpId/": typeof AuthProjectsProjectIdNetworkFloatingipsFloatingIpIdIndexRoute
238246
"/projects/$projectId/network/securitygroups/$securityGroupId/": typeof AuthProjectsProjectIdNetworkSecuritygroupsSecurityGroupIdIndexRoute
239247
"/projects/$projectId/services/pca/$pcaId/": typeof AuthProjectsProjectIdServicesPcaPcaIdIndexRoute
@@ -260,6 +268,7 @@ export interface FileRoutesByTo {
260268
"/projects/$projectId/network/floatingips": typeof AuthProjectsProjectIdNetworkFloatingipsIndexRoute
261269
"/projects/$projectId/network/securitygroups": typeof AuthProjectsProjectIdNetworkSecuritygroupsIndexRoute
262270
"/projects/$projectId/services/pca": typeof AuthProjectsProjectIdServicesPcaIndexRoute
271+
"/projects/$projectId/services/pca/$pcaId/$certificateId": typeof AuthProjectsProjectIdServicesPcaPcaIdCertificateIdRoute
263272
"/projects/$projectId/network/floatingips/$floatingIpId": typeof AuthProjectsProjectIdNetworkFloatingipsFloatingIpIdIndexRoute
264273
"/projects/$projectId/network/securitygroups/$securityGroupId": typeof AuthProjectsProjectIdNetworkSecuritygroupsSecurityGroupIdIndexRoute
265274
"/projects/$projectId/services/pca/$pcaId": typeof AuthProjectsProjectIdServicesPcaPcaIdIndexRoute
@@ -292,6 +301,7 @@ export interface FileRoutesById {
292301
"/_auth/projects/$projectId/network/floatingips/": typeof AuthProjectsProjectIdNetworkFloatingipsIndexRoute
293302
"/_auth/projects/$projectId/network/securitygroups/": typeof AuthProjectsProjectIdNetworkSecuritygroupsIndexRoute
294303
"/_auth/projects/$projectId/services/pca/": typeof AuthProjectsProjectIdServicesPcaIndexRoute
304+
"/_auth/projects/$projectId/services/pca/$pcaId/$certificateId": typeof AuthProjectsProjectIdServicesPcaPcaIdCertificateIdRoute
295305
"/_auth/projects/$projectId/network/floatingips/$floatingIpId/": typeof AuthProjectsProjectIdNetworkFloatingipsFloatingIpIdIndexRoute
296306
"/_auth/projects/$projectId/network/securitygroups/$securityGroupId/": typeof AuthProjectsProjectIdNetworkSecuritygroupsSecurityGroupIdIndexRoute
297307
"/_auth/projects/$projectId/services/pca/$pcaId/": typeof AuthProjectsProjectIdServicesPcaPcaIdIndexRoute
@@ -324,6 +334,7 @@ export interface FileRouteTypes {
324334
| "/projects/$projectId/network/floatingips/"
325335
| "/projects/$projectId/network/securitygroups/"
326336
| "/projects/$projectId/services/pca/"
337+
| "/projects/$projectId/services/pca/$pcaId/$certificateId"
327338
| "/projects/$projectId/network/floatingips/$floatingIpId/"
328339
| "/projects/$projectId/network/securitygroups/$securityGroupId/"
329340
| "/projects/$projectId/services/pca/$pcaId/"
@@ -350,6 +361,7 @@ export interface FileRouteTypes {
350361
| "/projects/$projectId/network/floatingips"
351362
| "/projects/$projectId/network/securitygroups"
352363
| "/projects/$projectId/services/pca"
364+
| "/projects/$projectId/services/pca/$pcaId/$certificateId"
353365
| "/projects/$projectId/network/floatingips/$floatingIpId"
354366
| "/projects/$projectId/network/securitygroups/$securityGroupId"
355367
| "/projects/$projectId/services/pca/$pcaId"
@@ -381,6 +393,7 @@ export interface FileRouteTypes {
381393
| "/_auth/projects/$projectId/network/floatingips/"
382394
| "/_auth/projects/$projectId/network/securitygroups/"
383395
| "/_auth/projects/$projectId/services/pca/"
396+
| "/_auth/projects/$projectId/services/pca/$pcaId/$certificateId"
384397
| "/_auth/projects/$projectId/network/floatingips/$floatingIpId/"
385398
| "/_auth/projects/$projectId/network/securitygroups/$securityGroupId/"
386399
| "/_auth/projects/$projectId/services/pca/$pcaId/"
@@ -592,6 +605,13 @@ declare module "@tanstack/react-router" {
592605
preLoaderRoute: typeof AuthProjectsProjectIdNetworkFloatingipsFloatingIpIdIndexRouteImport
593606
parentRoute: typeof AuthProjectsProjectIdNetworkRoute
594607
}
608+
"/_auth/projects/$projectId/services/pca/$pcaId/$certificateId": {
609+
id: "/_auth/projects/$projectId/services/pca/$pcaId/$certificateId"
610+
path: "/services/pca/$pcaId/$certificateId"
611+
fullPath: "/projects/$projectId/services/pca/$pcaId/$certificateId"
612+
preLoaderRoute: typeof AuthProjectsProjectIdServicesPcaPcaIdCertificateIdRouteImport
613+
parentRoute: typeof AuthProjectsProjectIdRoute
614+
}
595615
"/_auth/projects/$projectId/storage/$provider/containers/$containerName/objects/": {
596616
id: "/_auth/projects/$projectId/storage/$provider/containers/$containerName/objects/"
597617
path: "/storage/$provider/containers/$containerName/objects"
@@ -678,6 +698,7 @@ interface AuthProjectsProjectIdRouteChildren {
678698
AuthProjectsProjectIdComputeIndexRoute: typeof AuthProjectsProjectIdComputeIndexRoute
679699
AuthProjectsProjectIdServicesIndexRoute: typeof AuthProjectsProjectIdServicesIndexRoute
680700
AuthProjectsProjectIdServicesPcaIndexRoute: typeof AuthProjectsProjectIdServicesPcaIndexRoute
701+
AuthProjectsProjectIdServicesPcaPcaIdCertificateIdRoute: typeof AuthProjectsProjectIdServicesPcaPcaIdCertificateIdRoute
681702
AuthProjectsProjectIdServicesPcaPcaIdIndexRoute: typeof AuthProjectsProjectIdServicesPcaPcaIdIndexRoute
682703
AuthProjectsProjectIdStorageProviderContainersIndexRoute: typeof AuthProjectsProjectIdStorageProviderContainersIndexRoute
683704
AuthProjectsProjectIdStorageProviderContainersContainerNameObjectsIndexRoute: typeof AuthProjectsProjectIdStorageProviderContainersContainerNameObjectsIndexRoute
@@ -701,6 +722,8 @@ const AuthProjectsProjectIdRouteChildren: AuthProjectsProjectIdRouteChildren = {
701722
AuthProjectsProjectIdServicesIndexRoute,
702723
AuthProjectsProjectIdServicesPcaIndexRoute:
703724
AuthProjectsProjectIdServicesPcaIndexRoute,
725+
AuthProjectsProjectIdServicesPcaPcaIdCertificateIdRoute:
726+
AuthProjectsProjectIdServicesPcaPcaIdCertificateIdRoute,
704727
AuthProjectsProjectIdServicesPcaPcaIdIndexRoute:
705728
AuthProjectsProjectIdServicesPcaPcaIdIndexRoute,
706729
AuthProjectsProjectIdStorageProviderContainersIndexRoute:
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import { describe, it, expect, vi, beforeEach } from "vitest"
2+
import { render, screen } 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 { trpcReact } from "@/client/trpcClient"
8+
import { RouteComponent } from "./$certificateId"
9+
10+
const mockSetPageTitle = vi.fn()
11+
const mockNavigate = vi.fn()
12+
13+
vi.mock("@tanstack/react-router", () => ({
14+
createFileRoute: () => () => ({
15+
staticData: {},
16+
useParams: () => ({ projectId: "project-1", pcaId: "ca-1", certificateId: "cert-1" }),
17+
useRouteContext: () => ({ setPageTitle: mockSetPageTitle }),
18+
}),
19+
useNavigate: () => mockNavigate,
20+
}))
21+
22+
vi.mock("@/client/trpcClient", async (importOriginal) => {
23+
const actual = await importOriginal<typeof import("@/client/trpcClient")>()
24+
return {
25+
...actual,
26+
trpcReact: {
27+
...actual.trpcReact,
28+
services: {
29+
...actual.trpcReact.services,
30+
pca: {
31+
...actual.trpcReact.services.pca,
32+
getByIdCertificate: {
33+
...actual.trpcReact.services.pca.getByIdCertificate,
34+
useQuery: vi.fn(),
35+
},
36+
},
37+
},
38+
},
39+
}
40+
})
41+
42+
const renderComponent = () =>
43+
render(
44+
<I18nProvider i18n={i18n}>
45+
<PortalProvider>
46+
<RouteComponent />
47+
</PortalProvider>
48+
</I18nProvider>
49+
)
50+
51+
const mockQuery = (overrides: object) =>
52+
vi.mocked(trpcReact.services.pca.getByIdCertificate.useQuery).mockReturnValue({
53+
data: undefined,
54+
isLoading: false,
55+
isError: false,
56+
error: null,
57+
...overrides,
58+
} as never)
59+
60+
describe("$certificateId page", () => {
61+
beforeEach(() => {
62+
vi.clearAllMocks()
63+
})
64+
65+
it("renders loading state", () => {
66+
mockQuery({ isLoading: true })
67+
renderComponent()
68+
69+
expect(screen.getByText("Loading Certificate Details...")).toBeInTheDocument()
70+
})
71+
72+
it("renders error state with message", () => {
73+
mockQuery({ isError: true, error: { message: "Not found" } })
74+
renderComponent()
75+
76+
expect(screen.getByText("Not found")).toBeInTheDocument()
77+
expect(screen.getByRole("button", { name: "Back to Certificate Authorities Details page" })).toBeInTheDocument()
78+
})
79+
80+
it("renders not-found state", () => {
81+
mockQuery({ data: undefined })
82+
renderComponent()
83+
84+
expect(screen.getByText("Certificate not found")).toBeInTheDocument()
85+
})
86+
87+
it("renders certificate details", () => {
88+
mockQuery({
89+
data: {
90+
id: "cert-1",
91+
certificate_authority_id: "ca-1",
92+
project_id: "project-1",
93+
csr: "-----BEGIN CERTIFICATE REQUEST-----\nABC\n-----END CERTIFICATE REQUEST-----",
94+
configuration: { validity: { not_before: 0, not_after: 86400 } },
95+
},
96+
})
97+
renderComponent()
98+
99+
expect(screen.getByText("cert-1 Certificate Details")).toBeInTheDocument()
100+
expect(screen.getByText("ca-1")).toBeInTheDocument()
101+
expect(screen.getByText("1 days")).toBeInTheDocument()
102+
})
103+
104+
it("navigates back on error back button click", async () => {
105+
const user = userEvent.setup()
106+
mockQuery({ isError: true, error: { message: "error" } })
107+
renderComponent()
108+
109+
await user.click(screen.getByRole("button", { name: "Back to Certificate Authorities Details page" }))
110+
111+
expect(mockNavigate).toHaveBeenCalledWith({
112+
to: "/projects/$projectId/services/pca/$pcaId",
113+
params: { projectId: "project-1", pcaId: "ca-1" },
114+
})
115+
})
116+
})
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import { createFileRoute, useNavigate } from "@tanstack/react-router"
2+
import type { RouteInfo } from "@/client/routes/routeInfo"
3+
import { trpcReact } from "@/client/trpcClient"
4+
import {
5+
Button,
6+
DescriptionDefinition,
7+
DescriptionList,
8+
DescriptionTerm,
9+
Divider,
10+
Spinner,
11+
Stack,
12+
} from "@cloudoperators/juno-ui-components/index"
13+
import { Trans, useLingui } from "@lingui/react/macro"
14+
import { MdContentCopy, MdDownload } from "react-icons/md"
15+
import { Fragment } from "react/jsx-runtime"
16+
17+
export const Route = createFileRoute("/_auth/projects/$projectId/services/pca/$pcaId/$certificateId")({
18+
staticData: { section: "services", service: "pca" } satisfies RouteInfo,
19+
component: RouteComponent,
20+
})
21+
22+
export function RouteComponent() {
23+
const { t } = useLingui()
24+
const navigate = useNavigate()
25+
const { setPageTitle } = Route.useRouteContext()
26+
const { projectId, pcaId, certificateId } = Route.useParams()
27+
28+
const {
29+
isLoading,
30+
isError,
31+
error,
32+
data: certificate,
33+
} = trpcReact.services.pca.getByIdCertificate.useQuery({
34+
project_id: projectId,
35+
certificate_authority_id: pcaId,
36+
certificate_id: certificateId,
37+
})
38+
39+
// Loading state
40+
if (isLoading) {
41+
setPageTitle(t`Loading...`)
42+
return (
43+
<Stack className="fixed inset-0" distribution="center" alignment="center" direction="vertical">
44+
<Spinner variant="primary" size="large" className="mb-2" />
45+
<Trans>Loading Certificate Details...</Trans>
46+
</Stack>
47+
)
48+
}
49+
50+
const handleBack = () =>
51+
navigate({
52+
to: "/projects/$projectId/services/pca/$pcaId",
53+
params: { projectId, pcaId },
54+
})
55+
56+
// Error state
57+
if (isError) {
58+
const errorMessage = error?.message || "Unknown error"
59+
return (
60+
<Stack className="fixed inset-0" distribution="center" alignment="center" direction="vertical" gap="5">
61+
<p className="text-theme-error font-semibold">
62+
<Trans>Error loading Certificate</Trans>
63+
</p>
64+
<p className="text-theme-highest">{errorMessage}</p>
65+
<Button onClick={handleBack} variant="primary">
66+
<Trans>Back to Certificate Authorities Details page</Trans>
67+
</Button>
68+
</Stack>
69+
)
70+
}
71+
72+
// No data state
73+
if (!certificate) {
74+
return (
75+
<Stack className="fixed inset-0" distribution="center" alignment="center" direction="vertical" gap="5">
76+
<p className="text-theme-secondary">
77+
<Trans>Certificate not found</Trans>
78+
</p>
79+
<Button onClick={handleBack} variant="primary">
80+
<Trans>Back to Certificate Authorities Details page</Trans>
81+
</Button>
82+
</Stack>
83+
)
84+
}
85+
86+
setPageTitle(`Certificate ${certificate.id}`)
87+
88+
const basicInfo = [
89+
{ label: t`CA ID`, value: certificate.certificate_authority_id },
90+
{ label: t`ID`, value: certificate.id },
91+
{
92+
label: t`Duration/validity`,
93+
value:
94+
certificate.configuration?.validity.not_before !== undefined &&
95+
certificate.configuration?.validity.not_after !== undefined
96+
? `${Math.round(
97+
(certificate.configuration.validity.not_after - certificate.configuration.validity.not_before) /
98+
(60 * 60 * 24)
99+
)} days`
100+
: undefined,
101+
},
102+
] as const
103+
104+
return (
105+
<Stack direction="vertical" gap="3">
106+
<div className="text-theme-default text-2xl font-semibold">{`${certificate.id} Certificate Details`}</div>
107+
108+
<p className="text-theme-highest text-sm">
109+
<Trans>Manage your Certificate</Trans>
110+
</p>
111+
112+
<Stack gap="4" className="grid grid-cols-2 items-start">
113+
<DescriptionList alignTerms="right" className="w-full">
114+
{basicInfo.map(({ label, value }) => (
115+
<Fragment key={label}>
116+
<DescriptionTerm>{label}</DescriptionTerm>
117+
<DescriptionDefinition>{value || "—"}</DescriptionDefinition>
118+
</Fragment>
119+
))}
120+
</DescriptionList>
121+
122+
<div className="bg-dt-background w-full rounded-sm">
123+
<div className="text-theme-default p-4 text-xl font-bold">Certificate {`${certificate.id}`}</div>
124+
<Divider />
125+
126+
<div className="p-4 text-sm break-all whitespace-pre-wrap">{certificate?.csr}</div>
127+
128+
{/* I will implement downloading-copying functionality at issue/import part of the epic as I need to clarify some stuff with design-clavis team */}
129+
<Divider />
130+
<Stack gap="2" distribution="end" className="p-4">
131+
<Button>
132+
<MdDownload />
133+
</Button>
134+
<Button>
135+
<MdContentCopy />
136+
</Button>
137+
</Stack>
138+
</div>
139+
</Stack>
140+
</Stack>
141+
)
142+
}

0 commit comments

Comments
 (0)