Files
stack/apps/web/src/components/credentials/CreateCredentialDialog.tsx
Jason Woltje e8a9a3087a
All checks were successful
ci/woodpecker/push/orchestrator Pipeline was successful
ci/woodpecker/push/web Pipeline was successful
fix(ci): fix pipeline #366 — web @mosaic/ui build, Dockerfile find bug, event handler types
Three root causes resolved:

1. .woodpecker/web.yml: build-shared step was missing @mosaic/ui build,
   causing 10 test suite failures + 20 typecheck errors (TS2307)

2. apps/orchestrator/Dockerfile: find -o without parentheses only deleted
   last pattern's matches, leaving spec files with test fixture secrets
   that triggered 5 Trivy false positives (3 CRITICAL, 2 HIGH)

3. 9 web files had untyped event handler parameters (e) causing 49 lint
   errors and 19 typecheck errors — added React.ChangeEvent<T> types

Verification: lint 0 errors, typecheck 0 errors, tests 73/73 suites pass

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 17:50:41 -06:00

248 lines
7.7 KiB
TypeScript

"use client";
import { useState } from "react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { CredentialType, CredentialScope } from "@/lib/api/credentials";
interface CreateCredentialDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onSubmit: (data: CreateCredentialFormData) => Promise<void>;
}
export interface CreateCredentialFormData {
name: string;
provider: string;
type: CredentialType;
scope: CredentialScope;
value: string;
description?: string;
expiresAt?: string;
}
const PROVIDERS = [
{ value: "github", label: "GitHub" },
{ value: "gitlab", label: "GitLab" },
{ value: "bitbucket", label: "Bitbucket" },
{ value: "openai", label: "OpenAI" },
{ value: "custom", label: "Custom" },
];
export function CreateCredentialDialog({
open,
onOpenChange,
onSubmit,
}: CreateCredentialDialogProps): React.ReactElement {
const [formData, setFormData] = useState<CreateCredentialFormData>({
name: "",
provider: "custom",
type: CredentialType.API_KEY,
scope: CredentialScope.USER,
value: "",
description: "",
expiresAt: "",
});
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleSubmit = async (e: React.SyntheticEvent): Promise<void> => {
e.preventDefault();
setError(null);
// Validation
if (!formData.name.trim()) {
setError("Name is required");
return;
}
if (!formData.value.trim()) {
setError("Value is required");
return;
}
setIsSubmitting(true);
try {
await onSubmit(formData);
// Reset form on success
setFormData({
name: "",
provider: "custom",
type: CredentialType.API_KEY,
scope: CredentialScope.USER,
value: "",
description: "",
expiresAt: "",
});
onOpenChange(false);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to create credential");
} finally {
setIsSubmitting(false);
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[525px]">
<form onSubmit={handleSubmit}>
<DialogHeader>
<DialogTitle>Add Credential</DialogTitle>
<DialogDescription>
Store a new credential securely. All values are encrypted at rest.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
{/* Name */}
<div className="grid gap-2">
<Label htmlFor="name">Name *</Label>
<Input
id="name"
value={formData.name}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setFormData({ ...formData, name: e.target.value });
}}
placeholder="e.g., GitHub Personal Token"
disabled={isSubmitting}
/>
</div>
{/* Provider */}
<div className="grid gap-2">
<Label htmlFor="provider">Provider</Label>
<Select
value={formData.provider}
onValueChange={(value) => {
setFormData({ ...formData, provider: value });
}}
disabled={isSubmitting}
>
<SelectTrigger id="provider">
<SelectValue />
</SelectTrigger>
<SelectContent>
{PROVIDERS.map((provider) => (
<SelectItem key={provider.value} value={provider.value}>
{provider.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Type */}
<div className="grid gap-2">
<Label htmlFor="type">Type</Label>
<Select
value={formData.type}
onValueChange={(value) => {
setFormData({ ...formData, type: value as CredentialType });
}}
disabled={isSubmitting}
>
<SelectTrigger id="type">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value={CredentialType.API_KEY}>API Key</SelectItem>
<SelectItem value={CredentialType.ACCESS_TOKEN}>Access Token</SelectItem>
<SelectItem value={CredentialType.OAUTH_TOKEN}>OAuth Token</SelectItem>
<SelectItem value={CredentialType.PASSWORD}>Password</SelectItem>
<SelectItem value={CredentialType.SECRET}>Secret</SelectItem>
<SelectItem value={CredentialType.CUSTOM}>Custom</SelectItem>
</SelectContent>
</Select>
</div>
{/* Value */}
<div className="grid gap-2">
<Label htmlFor="value">Value *</Label>
<Input
id="value"
type="password"
value={formData.value}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setFormData({ ...formData, value: e.target.value });
}}
placeholder="Enter credential value"
disabled={isSubmitting}
/>
<p className="text-xs text-muted-foreground">
This value will be encrypted and cannot be viewed in the list
</p>
</div>
{/* Description */}
<div className="grid gap-2">
<Label htmlFor="description">Description</Label>
<Textarea
id="description"
value={formData.description}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
setFormData({ ...formData, description: e.target.value });
}}
placeholder="Optional description"
disabled={isSubmitting}
rows={2}
/>
</div>
{/* Expiry Date */}
<div className="grid gap-2">
<Label htmlFor="expiresAt">Target Date (optional)</Label>
<Input
id="expiresAt"
type="date"
value={formData.expiresAt}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setFormData({ ...formData, expiresAt: e.target.value });
}}
disabled={isSubmitting}
/>
<p className="text-xs text-muted-foreground">
Consider rotating the credential by this date
</p>
</div>
{/* Error Display */}
{error && <div className="text-sm text-destructive">{error}</div>}
</div>
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={() => {
onOpenChange(false);
}}
disabled={isSubmitting}
>
Cancel
</Button>
<Button type="submit" disabled={isSubmitting}>
{isSubmitting ? "Creating..." : "Create Credential"}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}