ai_api / src /app /(dashboard) /dashboard /settings /components /FeatureFlagCard.tsx
Yogesh
initial deploy
cd8bd0a
Raw
History Blame Contribute Delete
7.88 kB
"use client";
interface FeatureFlagCardProps {
flag: {
key: string;
label: string;
description: string;
category: "security" | "network" | "policies" | "runtime" | "cli" | "health";
type: "boolean" | "enum";
enumValues?: string[] | null;
effectiveValue: string;
source: "db" | "env" | "default";
requiresRestart: boolean;
warningLevel?: "info" | "caution" | "danger";
};
onToggle: (key: string, newValue: string) => void;
onReset: (key: string) => void;
saving?: boolean;
}
const CATEGORY_STYLES: Record<
FeatureFlagCardProps["flag"]["category"],
{ bg: string; border: string; text: string; label: string }
> = {
security: {
bg: "bg-red-50 dark:bg-red-500/15",
border: "border-red-200 dark:border-red-500/20",
text: "text-red-700 dark:text-red-300",
label: "Security",
},
network: {
bg: "bg-sky-50 dark:bg-blue-500/15",
border: "border-sky-200 dark:border-blue-500/20",
text: "text-sky-700 dark:text-blue-300",
label: "Network",
},
policies: {
bg: "bg-amber-50 dark:bg-amber-500/15",
border: "border-amber-200 dark:border-amber-500/20",
text: "text-amber-700 dark:text-amber-300",
label: "Policies",
},
runtime: {
bg: "bg-violet-50 dark:bg-purple-500/15",
border: "border-violet-200 dark:border-purple-500/20",
text: "text-violet-700 dark:text-purple-300",
label: "Runtime",
},
cli: {
bg: "bg-emerald-50 dark:bg-green-500/15",
border: "border-emerald-200 dark:border-green-500/20",
text: "text-emerald-700 dark:text-green-300",
label: "CLI",
},
health: {
bg: "bg-cyan-50 dark:bg-cyan-500/15",
border: "border-cyan-200 dark:border-cyan-500/20",
text: "text-cyan-700 dark:text-cyan-300",
label: "Health",
},
};
const SOURCE_STYLES: Record<
FeatureFlagCardProps["flag"]["source"],
{ bg: string; border: string; text: string; label: string }
> = {
db: {
bg: "bg-sky-50 dark:bg-blue-500/20",
border: "border-sky-200 dark:border-blue-500/30",
text: "text-sky-700 dark:text-blue-300",
label: "DB",
},
env: {
bg: "bg-amber-50 dark:bg-amber-500/20",
border: "border-amber-200 dark:border-amber-500/30",
text: "text-amber-700 dark:text-amber-300",
label: "ENV",
},
default: {
bg: "bg-slate-100 dark:bg-slate-500/20",
border: "border-slate-200 dark:border-slate-500/30",
text: "text-slate-600 dark:text-slate-300",
label: "DEF",
},
};
function isEnabled(value: string): boolean {
return value === "true" || value === "1" || value === "yes";
}
function Spinner() {
return (
<span
className="inline-block h-4 w-4 animate-spin rounded-full border-2 border-border border-t-text-primary"
aria-hidden="true"
/>
);
}
export default function FeatureFlagCard({
flag,
onToggle,
onReset,
saving = false,
}: FeatureFlagCardProps) {
const enabled = flag.type === "boolean" ? isEnabled(flag.effectiveValue) : false;
const category = CATEGORY_STYLES[flag.category];
const source = SOURCE_STYLES[flag.source];
const cardBorder =
flag.type === "boolean" && enabled
? "border-emerald-300 shadow-emerald-500/10 dark:border-green-500/30"
: "border-border";
return (
<div
role="group"
aria-label={flag.label}
className={`rounded-xl border bg-card p-4 shadow-soft transition-all duration-200 hover:-translate-y-px hover:border-black/15 hover:bg-bg-subtle/60 hover:shadow-elevated dark:hover:border-white/15 dark:hover:bg-surface ${cardBorder}`}
>
{/* Top row: category badge + toggle/select */}
<div className="flex items-center justify-between mb-3">
<span
aria-label={`Category: ${flag.category}`}
className={`inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-medium ${category.bg} ${category.border} ${category.text}`}
>
{category.label}
</span>
<div className="flex items-center gap-2">
{saving && <Spinner />}
{flag.type === "boolean" ? (
<button
role="switch"
aria-checked={enabled}
aria-label={flag.label}
disabled={saving}
onClick={() => onToggle(flag.key, enabled ? "false" : "true")}
className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary disabled:cursor-not-allowed disabled:opacity-50 ${
enabled ? "bg-emerald-500" : "bg-slate-300 dark:bg-white/20"
}`}
>
<span
className={`inline-block h-3.5 w-3.5 transform rounded-full bg-white shadow transition-transform ${
enabled ? "translate-x-4" : "translate-x-0.5"
}`}
aria-hidden="true"
/>
</button>
) : (
<select
aria-label={flag.label}
disabled={saving}
value={flag.effectiveValue}
onChange={(e) => onToggle(flag.key, e.target.value)}
className="rounded-md border border-border bg-bg-subtle px-2 py-0.5 text-xs text-text-primary focus:outline-none focus:ring-1 focus:ring-primary disabled:cursor-not-allowed disabled:opacity-50"
>
{(flag.enumValues ?? []).map((val) => (
<option key={val} value={val} className="bg-card text-text-primary">
{val}
</option>
))}
</select>
)}
</div>
</div>
{/* Flag key + warning icon */}
<div className="flex items-center gap-2 mb-1.5">
<span className="flex-1 truncate font-mono text-xs font-semibold text-text-primary">
{flag.key}
</span>
{flag.warningLevel === "caution" && (
<span className="text-sm text-amber-500 dark:text-amber-300" aria-label="Caution">
⚠️
</span>
)}
{flag.warningLevel === "danger" && (
<span className="text-sm animate-pulse" aria-label="Danger">
🔴
</span>
)}
{flag.requiresRestart && (
<span
className="rounded border border-slate-300 bg-slate-50 px-1 text-[10px] text-slate-600 dark:border-slate-400/30 dark:bg-transparent dark:text-slate-300"
title="Requires restart"
aria-label="Requires restart"
>
restart
</span>
)}
</div>
{/* Description */}
<p className="mb-3 line-clamp-2 text-xs text-text-muted">{flag.description}</p>
{/* Bottom row: source badge + reset button */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-1.5">
<span className="text-xs text-text-muted">Source:</span>
<span
className={`inline-flex items-center rounded border px-1.5 py-0.5 font-mono text-xs font-medium ${source.bg} ${source.border} ${source.text}`}
>
{source.label}
</span>
</div>
{flag.source === "db" && (
<button
aria-label={`Reset ${flag.label} to default`}
disabled={saving}
onClick={() => onReset(flag.key)}
className="inline-flex items-center gap-1 rounded text-xs text-text-muted transition-colors hover:text-text-primary disabled:cursor-not-allowed disabled:opacity-40 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary"
>
<span className="material-symbols-outlined text-[14px]" aria-hidden="true">
refresh
</span>
Reset
</button>
)}
</div>
</div>
);
}