molecule-core/canvas/src/components/MissingKeysModal.tsx
UIUX Designer a4350121dd fix(wcag): sweep text-zinc-600→zinc-500 across 9 components with small text
zinc-600 on zinc-900/950 background ≈ 2.6:1 contrast (WCAG AA requires
4.5:1 for text under 18pt). Found 15 instances across 9 components where
small-text data labels used this low-contrast pairing.

Files and what they label:
  EmptyState.tsx:132     — skill count + model on template cards (new-user visible)
  SidePanel.tsx:230      — workspace ID in panel footer (copyable, functional)
  ActivityTab.tsx:210    — entry timestamp (8px)
  ActivityTab.tsx:214    — expand chevron affordance (9px)
  ActivityTab.tsx:236    — "→" direction arrow between agents (9px)
  ActivityTab.tsx:278    — entry ID (8px, font-mono)
  ScheduleTab.tsx:284    — empty-state description text (9px)
  ScheduleTab.tsx:320    — schedule prompt preview (9px, truncate)
  ScheduleTab.tsx:323    — last/next/run-count metadata row (8px)
  SkillsTab.tsx:380      — "Examples" section header (9px uppercase)
  TracesTab.tsx:132      — trace ID (8px, font-mono)
  AgentCommsPanel.tsx:166 — message timestamp (9px)
  secrets-section.tsx:59  — secret key name (9px, font-mono)
  secrets-section.tsx:308 — encryption notice (9px)
  MissingKeysModal.tsx:175 — missing key identifier (9px, font-mono)

Fix: zinc-600 → zinc-500 across all 15 instances. Purely cosmetic —
no logic, no layout, no interactive behaviour changed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 07:53:00 +00:00

259 lines
8.9 KiB
TypeScript

"use client";
import { useState, useEffect, useCallback } from "react";
import { api } from "@/lib/api";
import { getKeyLabel } from "@/lib/deploy-preflight";
interface Props {
open: boolean;
missingKeys: string[];
runtime: string;
/** Called when user adds all keys and wants to proceed with deploy. */
onKeysAdded: () => void;
/** Called when user cancels the deploy. */
onCancel: () => void;
/** Called when user wants to open the Settings Panel (Config tab → Secrets). */
onOpenSettings?: () => void;
/** Optional workspace ID — if provided, secrets are saved at workspace scope. */
workspaceId?: string;
}
interface KeyEntry {
key: string;
label: string;
value: string;
saved: boolean;
saving: boolean;
error: string | null;
}
export function MissingKeysModal({
open,
missingKeys,
runtime,
onKeysAdded,
onCancel,
onOpenSettings,
workspaceId,
}: Props) {
const [entries, setEntries] = useState<KeyEntry[]>([]);
const [globalError, setGlobalError] = useState<string | null>(null);
// Initialize entries when modal opens or missingKeys change
useEffect(() => {
if (!open) return;
setEntries(
missingKeys.map((key) => ({
key,
label: getKeyLabel(key),
value: "",
saved: false,
saving: false,
error: null,
})),
);
setGlobalError(null);
}, [open, missingKeys]);
// Keyboard handler
useEffect(() => {
if (!open) return;
const handler = (e: KeyboardEvent) => {
if (e.key === "Escape") onCancel();
};
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
}, [open, onCancel]);
const updateEntry = useCallback(
(index: number, updates: Partial<KeyEntry>) => {
setEntries((prev) =>
prev.map((entry, i) => (i === index ? { ...entry, ...updates } : entry)),
);
},
[],
);
const handleSaveKey = useCallback(
async (index: number) => {
const entry = entries[index];
if (!entry.value.trim()) return;
updateEntry(index, { saving: true, error: null });
try {
// Save to global scope by default (available to all workspaces)
if (workspaceId) {
await api.put(`/workspaces/${workspaceId}/secrets`, {
key: entry.key,
value: entry.value.trim(),
});
} else {
await api.put("/settings/secrets", {
key: entry.key,
value: entry.value.trim(),
});
}
updateEntry(index, { saved: true, saving: false });
} catch (e) {
updateEntry(index, {
saving: false,
error: e instanceof Error ? e.message : "Failed to save",
});
}
},
[entries, updateEntry, workspaceId],
);
const handleAddKeysAndDeploy = useCallback(() => {
const anySaving = entries.some((e) => e.saving);
if (anySaving) {
setGlobalError("Please wait for all keys to finish saving.");
return;
}
const allSaved = entries.every((e) => e.saved);
if (!allSaved) {
setGlobalError("Please save all required keys before deploying.");
return;
}
onKeysAdded();
}, [entries, onKeysAdded]);
if (!open) return null;
const allSaved = entries.every((e) => e.saved);
const anySaving = entries.some((e) => e.saving);
const runtimeLabel = runtime.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
{/* Backdrop */}
<div
className="absolute inset-0 bg-black/70 backdrop-blur-sm"
onClick={onCancel}
/>
{/* Dialog */}
<div className="relative bg-zinc-900 border border-zinc-700 rounded-xl shadow-2xl shadow-black/50 max-w-[440px] w-full mx-4 overflow-hidden">
{/* Header */}
<div className="px-5 py-4 border-b border-zinc-800">
<div className="flex items-center gap-2 mb-1">
<div className="w-5 h-5 rounded-md bg-amber-600/20 border border-amber-500/30 flex items-center justify-center">
<svg width="12" height="12" viewBox="0 0 12 12" fill="none">
<path
d="M6 1L11 10H1L6 1Z"
stroke="#fbbf24"
strokeWidth="1.2"
strokeLinejoin="round"
/>
<path d="M6 5V7" stroke="#fbbf24" strokeWidth="1.2" strokeLinecap="round" />
<circle cx="6" cy="8.5" r="0.5" fill="#fbbf24" />
</svg>
</div>
<h3 className="text-sm font-semibold text-zinc-100">
Missing API Keys
</h3>
</div>
<p className="text-[12px] text-zinc-400 leading-relaxed">
The <span className="text-amber-300 font-medium">{runtimeLabel}</span> runtime
requires the following keys to be configured before deploying.
</p>
</div>
{/* Body — key list */}
<div className="px-5 py-4 space-y-3 max-h-[50vh] overflow-y-auto">
{entries.map((entry, index) => (
<div
key={entry.key}
className="bg-zinc-800/50 rounded-lg px-3 py-2.5 border border-zinc-700/50"
>
<div className="flex items-center justify-between mb-1">
<div>
<div className="text-[11px] text-zinc-300 font-medium">
{entry.label}
</div>
<div className="text-[9px] font-mono text-zinc-500">
{entry.key}
</div>
</div>
{entry.saved && (
<span className="text-[9px] text-emerald-400 bg-emerald-900/30 px-1.5 py-0.5 rounded flex items-center gap-1">
<svg width="8" height="8" viewBox="0 0 8 8" fill="none">
<path d="M1.5 4L3.5 6L6.5 2" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
Saved
</span>
)}
</div>
{!entry.saved && (
<div className="flex gap-2 mt-2">
<input
value={entry.value}
onChange={(e) => updateEntry(index, { value: e.target.value.trimStart() })}
placeholder={entry.key.includes("API_KEY") ? "sk-..." : "Enter value"}
type="password"
autoFocus={index === 0}
onKeyDown={(e) => {
if (e.key === "Enter" && entry.value.trim()) {
handleSaveKey(index);
}
}}
className="flex-1 bg-zinc-900 border border-zinc-600 rounded px-2 py-1.5 text-[11px] text-zinc-100 font-mono focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500/20 transition-colors"
/>
<button
onClick={() => handleSaveKey(index)}
disabled={!entry.value.trim() || entry.saving}
className="px-3 py-1.5 bg-blue-600 hover:bg-blue-500 text-[11px] rounded text-white disabled:opacity-30 transition-colors shrink-0"
>
{entry.saving ? "..." : "Save"}
</button>
</div>
)}
{entry.error && (
<div className="mt-1.5 text-[10px] text-red-400">{entry.error}</div>
)}
</div>
))}
{globalError && (
<div className="px-3 py-2 bg-red-950/40 border border-red-800/50 rounded-lg text-[11px] text-red-400">
{globalError}
</div>
)}
</div>
{/* Footer */}
<div className="px-5 py-3 border-t border-zinc-800 bg-zinc-950/50 flex items-center justify-between gap-2">
<div>
{onOpenSettings && (
<button
onClick={onOpenSettings}
className="text-[11px] text-blue-400 hover:text-blue-300 transition-colors"
>
Open Settings Panel
</button>
)}
</div>
<div className="flex items-center gap-2">
<button
onClick={onCancel}
className="px-3.5 py-1.5 text-[12px] text-zinc-400 hover:text-zinc-200 bg-zinc-800 hover:bg-zinc-700 border border-zinc-700 rounded-lg transition-colors"
>
Cancel Deploy
</button>
<button
onClick={handleAddKeysAndDeploy}
disabled={!allSaved || anySaving}
className="px-3.5 py-1.5 text-[12px] bg-blue-600 hover:bg-blue-500 text-white rounded-lg transition-colors disabled:opacity-40"
>
{anySaving ? "Saving..." : allSaved ? "Deploy" : "Add Keys"}
</button>
</div>
</div>
</div>
</div>
);
}