feat(web): add credential management UI pages and components

Add credentials settings page, audit log page, CRUD dialog components
(create, view, edit, rotate), credential card, dialog UI component,
and API client for the M7-CredentialSecurity feature.

Refs #346

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-10 09:42:41 -06:00
parent ab64583951
commit 6a5a4e4de8
10 changed files with 1841 additions and 0 deletions

View File

@@ -0,0 +1,128 @@
import * as React from "react";
import { X } from "lucide-react";
export interface DialogProps {
open?: boolean;
onOpenChange?: (open: boolean) => void;
children?: React.ReactNode;
}
export interface DialogTriggerProps {
children?: React.ReactNode;
asChild?: boolean;
}
export interface DialogContentProps {
children?: React.ReactNode;
className?: string;
}
export interface DialogHeaderProps {
children?: React.ReactNode;
className?: string;
}
export interface DialogFooterProps {
children?: React.ReactNode;
className?: string;
}
export interface DialogTitleProps {
children?: React.ReactNode;
className?: string;
}
export interface DialogDescriptionProps {
children?: React.ReactNode;
className?: string;
}
const DialogContext = React.createContext<{
open?: boolean;
onOpenChange?: (open: boolean) => void;
}>({});
export function Dialog({ open, onOpenChange, children }: DialogProps): React.JSX.Element {
const contextValue: { open?: boolean; onOpenChange?: (open: boolean) => void } = {};
if (open !== undefined) {
contextValue.open = open;
}
if (onOpenChange !== undefined) {
contextValue.onOpenChange = onOpenChange;
}
return <DialogContext.Provider value={contextValue}>{children}</DialogContext.Provider>;
}
export function DialogTrigger({ children, asChild }: DialogTriggerProps): React.JSX.Element {
const { onOpenChange } = React.useContext(DialogContext);
if (asChild && React.isValidElement(children)) {
return React.cloneElement(children, {
onClick: () => onOpenChange?.(true),
} as React.HTMLAttributes<HTMLElement>);
}
return <div onClick={() => onOpenChange?.(true)}>{children}</div>;
}
export function DialogContent({
children,
className = "",
}: DialogContentProps): React.JSX.Element | null {
const { open, onOpenChange } = React.useContext(DialogContext);
if (!open) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
{/* Overlay */}
<div className="fixed inset-0 bg-black/50" onClick={() => onOpenChange?.(false)} />
{/* Dialog Content */}
<div
className={`relative z-50 w-full max-w-lg rounded-lg bg-white p-6 shadow-lg ${className}`}
>
{/* Close Button */}
<button
onClick={() => onOpenChange?.(false)}
className="absolute right-4 top-4 rounded-sm opacity-70 hover:opacity-100 transition-opacity"
>
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</button>
{children}
</div>
</div>
);
}
export function DialogHeader({ children, className = "" }: DialogHeaderProps): React.JSX.Element {
return <div className={`mb-4 ${className}`}>{children}</div>;
}
export function DialogFooter({ children, className = "" }: DialogFooterProps): React.JSX.Element {
return <div className={`mt-4 flex justify-end gap-2 ${className}`}>{children}</div>;
}
export function DialogTitle({ children, className = "" }: DialogTitleProps): React.JSX.Element {
return <h2 className={`text-lg font-semibold ${className}`}>{children}</h2>;
}
export function DialogDescription({
children,
className = "",
}: DialogDescriptionProps): React.JSX.Element {
return <p className={`text-sm text-gray-600 ${className}`}>{children}</p>;
}
export const DialogPortal = ({ children }: { children: React.ReactNode }): React.JSX.Element => (
<>{children}</>
);
export const DialogOverlay = ({ children }: { children: React.ReactNode }): React.JSX.Element => (
<>{children}</>
);
export const DialogClose = ({ children }: { children: React.ReactNode }): React.JSX.Element => (
<>{children}</>
);