Skip to content

Commit cce1fbe

Browse files
feat(clavis): design changes first iteration (#932)
* fix(clavis): design issues from marta Signed-off-by: Vladislav Schur <u.shchur@sap.com> * feat(ui): breadcrumb double nested paths Signed-off-by: Vladislav Schur <u.shchur@sap.com> * fix(clavis): update tests for new changes Signed-off-by: Vladislav Schur <u.shchur@sap.com> * fix(clavis): ai pr comments Signed-off-by: Vladislav Schur <u.shchur@sap.com> --------- Signed-off-by: Vladislav Schur <u.shchur@sap.com>
1 parent 1df292f commit cce1fbe

24 files changed

Lines changed: 174 additions & 121 deletions

packages/aurora/docs/0011_clavis.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ Implemented screens and interactions:
3232
- details page supports lifecycle action in `AWAITING_CERTIFICATE` state to import an externally signed certificate chain
3333
- certificate list view via `PcaCertificatesListContainer` displays certificates issued by a CA
3434
- certificates list shows CA ID and certificate ID columns with loading, error, and empty states
35-
- in `READY` state, certificate list provides "Issue End Entity Certificate" action and modal to issue end-entity certificates
35+
- in `READY` state, certificate list provides "Issue End-Entity Certificate" action and modal to issue end-entity certificates
3636
- individual certificate rows rendered via `PcaCertificatesTableRow` component, clicking a row navigates to the certificate detail page
3737
- 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
3838

packages/aurora/src/client/components/ProjectView/ProjectInfoBox.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,23 @@ export function ProjectInfoBox({ projectInfo }: ProjectInfoBoxProps) {
8585

8686
if (info.isDetail) {
8787
items.push({ label: resolvedLabel, onClick: () => navigate({ to: to as never, params: params as never }) })
88+
89+
if (info.intermediateCrumb) {
90+
const { to: iTo, useParamAsLabel: iParam, useParentTitleAsLabel } = info.intermediateCrumb
91+
const parentMatch = projectMatches[projectMatches.length - 2]
92+
const parentTitle = parentMatch?.meta?.find((m) => m != null && "title" in m)?.title as string | undefined
93+
const iLabel = useParentTitleAsLabel
94+
? (parentTitle ?? (iParam ? params[iParam] : undefined))
95+
: iParam
96+
? params[iParam]
97+
: undefined
98+
items.push(
99+
iTo
100+
? { label: iLabel, onClick: () => navigate({ to: iTo as never, params: params as never }) }
101+
: { label: iLabel }
102+
)
103+
}
104+
88105
const title = deepest.meta?.find((m) => m != null && "title" in m)?.title as string | undefined
89106
if (title) items.push({ label: title, active: true })
90107
} else {

packages/aurora/src/client/routes/_auth/projects/$projectId/services/pca/$pcaId/$certificateId.tsx

Lines changed: 22 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
1+
import { Fragment } from "react"
12
import { createFileRoute, useNavigate } from "@tanstack/react-router"
2-
import type { RouteInfo } from "@/client/routes/routeInfo"
3-
import { trpcReact } from "@/client/trpcClient"
3+
import { Trans, useLingui } from "@lingui/react/macro"
44
import {
55
Button,
6+
CodeBlock,
67
DescriptionDefinition,
78
DescriptionList,
89
DescriptionTerm,
9-
Divider,
1010
Spinner,
1111
Stack,
1212
} 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"
13+
import type { RouteInfo } from "@/client/routes/routeInfo"
14+
import { trpcReact } from "@/client/trpcClient"
1615

1716
export const Route = createFileRoute("/_auth/projects/$projectId/services/pca/$pcaId/$certificateId")({
1817
staticData: {
@@ -21,6 +20,11 @@ export const Route = createFileRoute("/_auth/projects/$projectId/services/pca/$p
2120
isDetail: true,
2221
sectionCrumb: { labelKey: "Services" },
2322
crumb: { labelKey: "PCA (Clavis)", to: "/projects/$projectId/services/pca" },
23+
intermediateCrumb: {
24+
useParentTitleAsLabel: true,
25+
useParamAsLabel: "pcaId",
26+
to: "/projects/$projectId/services/pca/$pcaId",
27+
},
2428
} satisfies RouteInfo,
2529
loader: async ({ context, params }) => {
2630
const cert = await context.trpcClient?.services.pca.getByIdCertificate.query({
@@ -98,9 +102,13 @@ export function RouteComponent() {
98102
)
99103
}
100104

105+
const certificateIdValue = certificate.id
106+
const certificateHeading = t`Certificate ${certificateIdValue}`
107+
const certificateDetails = t`${certificateIdValue} Certificate Details`
108+
101109
const basicInfo = [
102110
{ label: t`CA ID`, value: certificate.certificate_authority_id },
103-
{ label: t`ID`, value: certificate.id },
111+
{ label: t`ID`, value: certificateIdValue },
104112
{
105113
label: t`Duration/validity`,
106114
value:
@@ -116,7 +124,7 @@ export function RouteComponent() {
116124

117125
return (
118126
<Stack direction="vertical" gap="3">
119-
<div className="text-theme-default text-2xl font-semibold">{`${certificate.id} Certificate Details`}</div>
127+
<div className="text-theme-default text-2xl font-semibold">{certificateDetails}</div>
120128

121129
<p className="text-theme-highest text-sm">
122130
<Trans>Manage your Certificate</Trans>
@@ -132,23 +140,12 @@ export function RouteComponent() {
132140
))}
133141
</DescriptionList>
134142

135-
<div className="bg-dt-background w-full rounded-sm">
136-
<div className="text-theme-default p-4 text-xl font-bold">Certificate {`${certificate.id}`}</div>
137-
<Divider />
138-
139-
<div className="p-4 text-sm break-all whitespace-pre-wrap">{certificate?.csr}</div>
140-
141-
{/* I will implement downloading-copying functionality at issue/import part of the epic as I need to clarify some stuff with design-clavis team */}
142-
<Divider />
143-
<Stack gap="2" distribution="end" className="p-4">
144-
<Button>
145-
<MdDownload />
146-
</Button>
147-
<Button>
148-
<MdContentCopy />
149-
</Button>
150-
</Stack>
151-
</div>
143+
<CodeBlock
144+
heading={certificateHeading}
145+
content={certificate?.csr ?? ""}
146+
className="w-full [&_pre_code]:block [&_pre_code]:w-full"
147+
wrap
148+
/>
152149
</Stack>
153150
</Stack>
154151
)

packages/aurora/src/client/routes/_auth/projects/$projectId/services/pca/$pcaId/-components/-modals/IssueEndEntityCertificateModal.test.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,15 @@ import { PortalProvider } from "@cloudoperators/juno-ui-components"
77
import { IssueEndEntityCertificateModal } from "./IssueEndEntityCertificateModal"
88

99
const mockProjectId = "project-123"
10-
const mockMutateAsync = vi.fn().mockResolvedValue({})
10+
const mockNavigate = vi.fn()
11+
const mockMutateAsync = vi.fn().mockResolvedValue({ id: "cert-987" })
1112
const mockReset = vi.fn()
1213
const mockInvalidate = vi.fn()
1314

15+
vi.mock("@tanstack/react-router", () => ({
16+
useNavigate: () => mockNavigate,
17+
}))
18+
1419
vi.mock("@/client/hooks", () => ({
1520
useProjectId: () => mockProjectId,
1621
}))
@@ -103,5 +108,9 @@ describe("IssueEndEntityCertificateModal", () => {
103108
expect(mockInvalidate).toHaveBeenCalledTimes(1)
104109
expect(mockReset).toHaveBeenCalledTimes(1)
105110
expect(onClose).toHaveBeenCalledTimes(1)
111+
expect(mockNavigate).toHaveBeenCalledWith({
112+
to: "/projects/$projectId/services/pca/$pcaId/$certificateId",
113+
params: { projectId: "project-123", pcaId: "ca-1", certificateId: "cert-987" },
114+
})
106115
})
107116
})

packages/aurora/src/client/routes/_auth/projects/$projectId/services/pca/$pcaId/-components/-modals/IssueEndEntityCertificateModal.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { z } from "zod"
22
import { useForm, useStore } from "@tanstack/react-form"
3+
import { useNavigate } from "@tanstack/react-router"
34
import { Trans, useLingui } from "@lingui/react/macro"
45
import { Modal, Form, FormSection, Spinner, Message, Textarea } from "@cloudoperators/juno-ui-components"
56
import { trpcReact } from "@/client/trpcClient"
@@ -13,6 +14,7 @@ export interface IssueEndEntityCertificateModalProps {
1314

1415
export const IssueEndEntityCertificateModal = ({ open, onClose, pcaId }: IssueEndEntityCertificateModalProps) => {
1516
const { t } = useLingui()
17+
const navigate = useNavigate()
1618
const projectId = useProjectId()
1719
const utils = trpcReact.useUtils()
1820

@@ -34,14 +36,19 @@ export const IssueEndEntityCertificateModal = ({ open, onClose, pcaId }: IssueEn
3436
onSubmit: async ({ value }) => {
3537
if (isPending) return
3638

37-
await createCertificateMutation.mutateAsync({
39+
const createdCertificate = await createCertificateMutation.mutateAsync({
3840
project_id: projectId,
3941
certificate_authority_id: pcaId,
4042
// Normalize to one format so users can paste raw multi-line CSRs with \n along with already formatted ones
4143
csr: value.csr.replace(/\\n/g, "\n"),
4244
configuration: { validity: { not_after: Math.floor(Date.now() / 1000) + 8 * 60 * 60 } },
4345
})
4446
handleClose()
47+
48+
await navigate({
49+
to: "/projects/$projectId/services/pca/$pcaId/$certificateId",
50+
params: { projectId, pcaId, certificateId: createdCertificate.id },
51+
})
4552
},
4653
})
4754

@@ -59,7 +66,7 @@ export const IssueEndEntityCertificateModal = ({ open, onClose, pcaId }: IssueEn
5966
<Modal
6067
open={open}
6168
size="large"
62-
title={t`Issue End Entity Certificate`}
69+
title={t`Issue End-Entity Certificate`}
6370
onCancel={handleClose}
6471
cancelButtonLabel={t`Cancel`}
6572
confirmButtonLabel={t`Save`}
@@ -76,7 +83,7 @@ export const IssueEndEntityCertificateModal = ({ open, onClose, pcaId }: IssueEn
7683
<div className="mb-4 flex items-center justify-center gap-2">
7784
<Spinner variant="primary" />
7885
<span className="text-theme-high text-sm">
79-
<Trans>Issuing End Entity Certificate...</Trans>
86+
<Trans>Issuing End-Entity Certificate...</Trans>
8087
</span>
8188
</div>
8289
)}

packages/aurora/src/client/routes/_auth/projects/$projectId/services/pca/$pcaId/-components/-modals/IssueSelfSignedCertificateModal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export const IssueSelfSignedCertificateModal = ({ open, onClose, pca }: IssueSel
4747
return (
4848
<Modal
4949
open={open}
50-
title={t`Issue Self Signed Certificate`}
50+
title={t`Issue Self-Signed Certificate`}
5151
onCancel={handleClose}
5252
cancelButtonLabel={t`Cancel`}
5353
confirmButtonLabel={t`Issue Certificate`}

packages/aurora/src/client/routes/_auth/projects/$projectId/services/pca/$pcaId/-components/-table/PcaCertificatesTableRow.test.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,14 +64,17 @@ describe("PcaCertificatesTableRow", () => {
6464
})
6565
})
6666

67-
it("renders disabled Create Certificate menu item", async () => {
67+
it("navigates to certificate details page from the Show Details menu item", async () => {
6868
const user = userEvent.setup()
6969
renderRow(baseCertificate)
7070

7171
await user.click(screen.getByRole("button", { name: "More" }))
72+
await user.click(screen.getByRole("menuitem", { name: "Show Details" }))
7273

73-
const menuItem = screen.getByRole("menuitem", { name: "Create Certificate" })
74-
expect(menuItem).toBeInTheDocument()
75-
expect(menuItem).toHaveAttribute("aria-disabled", "true")
74+
expect(mockNavigate).toHaveBeenCalledWith({
75+
from: "/projects/$projectId/services/pca/$pcaId/",
76+
to: "$certificateId",
77+
params: expect.any(Function),
78+
})
7679
})
7780
})

packages/aurora/src/client/routes/_auth/projects/$projectId/services/pca/$pcaId/-components/-table/PcaCertificatesTableRow.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,7 @@ export const PcaCertificatesTableRow = ({ certificate }: PcaCertificatesTableRow
3535
<DataGridCell onClick={(e) => e.stopPropagation()} className="items-end pr-0">
3636
<PopupMenu>
3737
<PopupMenuOptions>
38-
{/* I will enable this button on create-certificate task of the EPIC */}
39-
<PopupMenuItem label={t`Create Certificate`} disabled />
38+
<PopupMenuItem label={t`Show Details`} onClick={navigateToCertificateDetailsPage} />
4039
</PopupMenuOptions>
4140
</PopupMenu>
4241
</DataGridCell>

packages/aurora/src/client/routes/_auth/projects/$projectId/services/pca/$pcaId/-components/PcaCertificatesListContainer.test.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ vi.mock("./-table/PcaCertificatesTableRow", () => ({
4141
}))
4242

4343
vi.mock("./-modals/IssueEndEntityCertificateModal", () => ({
44-
IssueEndEntityCertificateModal: ({ open }: { open: boolean }) => (open ? <div>Issue End Entity Modal</div> : null),
44+
IssueEndEntityCertificateModal: ({ open }: { open: boolean }) => (open ? <div>Issue End-Entity Modal</div> : null),
4545
}))
4646

4747
describe("PcaCertificatesListContainer", () => {
@@ -165,11 +165,11 @@ describe("PcaCertificatesListContainer", () => {
165165

166166
renderComponent()
167167

168-
const button = screen.getByRole("button", { name: "Issue End Entity Certificate" })
168+
const button = screen.getByRole("button", { name: "Issue End-Entity Certificate" })
169169
expect(button).toBeInTheDocument()
170170

171171
await user.click(button)
172-
expect(screen.getByText("Issue End Entity Modal")).toBeInTheDocument()
172+
expect(screen.getByText("Issue End-Entity Modal")).toBeInTheDocument()
173173
})
174174

175175
it("does not show issue certificate action when state is not READY", () => {
@@ -182,7 +182,7 @@ describe("PcaCertificatesListContainer", () => {
182182

183183
renderComponent("AWAITING_CERTIFICATE")
184184

185-
expect(screen.queryByRole("button", { name: "Issue End Entity Certificate" })).not.toBeInTheDocument()
185+
expect(screen.queryByRole("button", { name: "Issue End-Entity Certificate" })).not.toBeInTheDocument()
186186
})
187187

188188
it("shows issue certificate action in empty state when READY and opens modal", async () => {
@@ -197,11 +197,11 @@ describe("PcaCertificatesListContainer", () => {
197197

198198
renderComponent()
199199

200-
const button = screen.getByRole("button", { name: "Issue End Entity Certificate" })
200+
const button = screen.getByRole("button", { name: "Issue End-Entity Certificate" })
201201
expect(button).toBeInTheDocument()
202202

203203
await user.click(button)
204-
expect(screen.getByText("Issue End Entity Modal")).toBeInTheDocument()
204+
expect(screen.getByText("Issue End-Entity Modal")).toBeInTheDocument()
205205
})
206206

207207
it("does not show issue certificate action in empty state when state is not READY", () => {
@@ -214,7 +214,7 @@ describe("PcaCertificatesListContainer", () => {
214214

215215
renderComponent("AWAITING_CERTIFICATE")
216216

217-
expect(screen.queryByRole("button", { name: "Issue End Entity Certificate" })).not.toBeInTheDocument()
217+
expect(screen.queryByRole("button", { name: "Issue End-Entity Certificate" })).not.toBeInTheDocument()
218218
})
219219

220220
it("renders correct column headers", () => {

packages/aurora/src/client/routes/_auth/projects/$projectId/services/pca/$pcaId/-components/PcaCertificatesListContainer.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export const PcaCertificatesListContainer = ({ pcaId, pcaState }: PcaCertificate
3232
t`ID`,
3333
"", // empty column for item-action with context menu containing "Delete CA" button
3434
] as const
35+
const columnsLength = columns().length
3536

3637
const {
3738
data: pcaCertificates = [],
@@ -64,7 +65,9 @@ export const PcaCertificatesListContainer = ({ pcaId, pcaState }: PcaCertificate
6465
<div className="relative">
6566
{pcaState === "READY" && (
6667
<>
67-
<Button variant="primary" label={t`Issue End Entity Certificate`} onClick={toggleIssueEndEntity} />
68+
<Stack className="pt-3 pb-2" distribution="end">
69+
<Button variant="primary" label={t`Issue End-Entity Certificate`} onClick={toggleIssueEndEntity} />
70+
</Stack>
6871
{createIssueEndEntityOpen && (
6972
<IssueEndEntityCertificateModal
7073
open={createIssueEndEntityOpen}
@@ -76,9 +79,9 @@ export const PcaCertificatesListContainer = ({ pcaId, pcaState }: PcaCertificate
7679
)}
7780

7881
{pcaCertificates.length === 0 ? (
79-
<DataGrid columns={columns().length} className="pca-certificates" data-testid="no-pcas-certificates">
82+
<DataGrid columns={columnsLength} className="pca-certificates" data-testid="no-pcas-certificates">
8083
<DataGridRow>
81-
<DataGridCell colSpan={columns().length}>
84+
<DataGridCell colSpan={columnsLength}>
8285
<ContentHeading>
8386
<Trans>No Certificates issued by this Certificate Authority found</Trans>
8487
</ContentHeading>
@@ -89,7 +92,7 @@ export const PcaCertificatesListContainer = ({ pcaId, pcaState }: PcaCertificate
8992
</DataGridRow>
9093
</DataGrid>
9194
) : (
92-
<DataGrid columns={columns().length}>
95+
<DataGrid columns={columnsLength}>
9396
<DataGridRow>
9497
{columns().map((label) => (
9598
<DataGridHeadCell key={label}>{label}</DataGridHeadCell>

0 commit comments

Comments
 (0)