feat: add sidebar

This commit is contained in:
Austin Pickett 2026-04-22 23:25:17 -04:00
parent 7db2703b33
commit e5d2815b41
41 changed files with 2469 additions and 1391 deletions

View file

@ -1,4 +1,4 @@
import { useEffect, useState, useMemo } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import {
Eye,
EyeOff,
@ -16,8 +16,10 @@ import {
} from "lucide-react";
import { api } from "@/lib/api";
import type { EnvVarInfo } from "@/lib/api";
import { useToast } from "@/hooks/useToast";
import { DeleteConfirmDialog } from "@/components/DeleteConfirmDialog";
import { Toast } from "@/components/Toast";
import { useConfirmDelete } from "@/hooks/useConfirmDelete";
import { useToast } from "@/hooks/useToast";
import { OAuthProvidersCard } from "@/components/OAuthProvidersCard";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
@ -95,6 +97,7 @@ function EnvVarRow({
onClear,
onReveal,
onCancelEdit,
clearDialogOpen = false,
compact = false,
}: {
varKey: string;
@ -107,6 +110,7 @@ function EnvVarRow({
onClear: (key: string) => void;
onReveal: (key: string) => void;
onCancelEdit: (key: string) => void;
clearDialogOpen?: boolean;
compact?: boolean;
}) {
const { t } = useI18n();
@ -219,7 +223,7 @@ function EnvVarRow({
{info.is_set && (
<Button size="sm" variant="ghost"
className="text-destructive hover:text-destructive hover:bg-destructive/10"
onClick={() => onClear(varKey)} disabled={saving === varKey}>
onClick={() => onClear(varKey)} disabled={saving === varKey || clearDialogOpen}>
<Trash2 className="h-3 w-3" />
{saving === varKey ? "..." : t.common.clear}
</Button>
@ -261,6 +265,7 @@ function ProviderGroupCard({
onClear,
onReveal,
onCancelEdit,
clearDialogOpen = false,
}: {
group: ProviderGroup;
edits: Record<string, string>;
@ -271,6 +276,7 @@ function ProviderGroupCard({
onClear: (key: string) => void;
onReveal: (key: string) => void;
onCancelEdit: (key: string) => void;
clearDialogOpen?: boolean;
}) {
const [expanded, setExpanded] = useState(false);
const { t } = useI18n();
@ -325,6 +331,7 @@ function ProviderGroupCard({
key={key} varKey={key} info={info} compact
edits={edits} setEdits={setEdits} revealed={revealed} saving={saving}
onSave={onSave} onClear={onClear} onReveal={onReveal} onCancelEdit={onCancelEdit}
clearDialogOpen={clearDialogOpen}
/>
))}
{/* Base URLs (secondary) */}
@ -333,6 +340,7 @@ function ProviderGroupCard({
key={key} varKey={key} info={info} compact
edits={edits} setEdits={setEdits} revealed={revealed} saving={saving}
onSave={onSave} onClear={onClear} onReveal={onReveal} onCancelEdit={onCancelEdit}
clearDialogOpen={clearDialogOpen}
/>
))}
{/* Anything else */}
@ -341,6 +349,7 @@ function ProviderGroupCard({
key={key} varKey={key} info={info} compact
edits={edits} setEdits={setEdits} revealed={revealed} saving={saving}
onSave={onSave} onClear={onClear} onReveal={onReveal} onCancelEdit={onCancelEdit}
clearDialogOpen={clearDialogOpen}
/>
))}
</div>
@ -390,24 +399,30 @@ export default function EnvPage() {
}
};
const handleClear = async (key: string) => {
setSaving(key);
try {
await api.deleteEnvVar(key);
setVars((prev) =>
prev
? { ...prev, [key]: { ...prev[key], is_set: false, redacted_value: null } }
: prev,
);
setEdits((prev) => { const n = { ...prev }; delete n[key]; return n; });
setRevealed((prev) => { const n = { ...prev }; delete n[key]; return n; });
showToast(`${key} ${t.common.removed}`, "success");
} catch (e) {
showToast(`${t.common.failedToRemove} ${key}: ${e}`, "error");
} finally {
setSaving(null);
}
};
const keyClear = useConfirmDelete({
onDelete: useCallback(
async (key: string) => {
setSaving(key);
try {
await api.deleteEnvVar(key);
setVars((prev) =>
prev
? { ...prev, [key]: { ...prev[key], is_set: false, redacted_value: null } }
: prev,
);
setEdits((prev) => { const n = { ...prev }; delete n[key]; return n; });
setRevealed((prev) => { const n = { ...prev }; delete n[key]; return n; });
showToast(`${key} ${t.common.removed}`, "success");
} catch (e) {
showToast(`${t.common.failedToRemove} ${key}: ${e}`, "error");
throw e;
} finally {
setSaving(null);
}
},
[showToast, t.common.removed, t.common.failedToRemove],
),
});
const handleReveal = async (key: string) => {
if (revealed[key]) {
@ -488,10 +503,29 @@ export default function EnvPage() {
const totalProviders = providerGroups.length;
const configuredProviders = providerGroups.filter((g) => g.hasAnySet).length;
const pendingClearKey = keyClear.pendingId;
const pendingKeyDescription =
pendingClearKey && vars
? vars[pendingClearKey]?.description
: undefined;
return (
<div className="flex flex-col gap-6">
<Toast toast={toast} />
<DeleteConfirmDialog
open={keyClear.isOpen}
onCancel={keyClear.cancel}
onConfirm={keyClear.confirm}
title={t.env.confirmClearTitle}
description={
pendingClearKey
? `${pendingClearKey}${pendingKeyDescription ? `${pendingKeyDescription}` : ""}. ${t.env.confirmClearMessage}`
: t.env.confirmClearMessage
}
loading={keyClear.isDeleting}
/>
<div className="flex items-center justify-between">
<div className="flex flex-col gap-1">
<p className="text-sm text-muted-foreground">
@ -530,7 +564,8 @@ export default function EnvPage() {
key={group.name}
group={group}
edits={edits} setEdits={setEdits} revealed={revealed} saving={saving}
onSave={handleSave} onClear={handleClear} onReveal={handleReveal} onCancelEdit={cancelEdit}
onSave={handleSave} onClear={keyClear.requestDelete} onReveal={handleReveal} onCancelEdit={cancelEdit}
clearDialogOpen={keyClear.isOpen}
/>
))}
</CardContent>
@ -557,7 +592,8 @@ export default function EnvPage() {
<EnvVarRow
key={key} varKey={key} info={info}
edits={edits} setEdits={setEdits} revealed={revealed} saving={saving}
onSave={handleSave} onClear={handleClear} onReveal={handleReveal} onCancelEdit={cancelEdit}
onSave={handleSave} onClear={keyClear.requestDelete} onReveal={handleReveal} onCancelEdit={cancelEdit}
clearDialogOpen={keyClear.isOpen}
/>
))}
@ -566,7 +602,8 @@ export default function EnvPage() {
category={category}
unsetEntries={unsetEntries}
edits={edits} setEdits={setEdits} revealed={revealed} saving={saving}
onSave={handleSave} onClear={handleClear} onReveal={handleReveal} onCancelEdit={cancelEdit}
onSave={handleSave} onClear={keyClear.requestDelete} onReveal={handleReveal} onCancelEdit={cancelEdit}
clearDialogOpen={keyClear.isOpen}
/>
)}
</CardContent>
@ -592,6 +629,7 @@ function CollapsibleUnset({
onClear,
onReveal,
onCancelEdit,
clearDialogOpen = false,
}: {
category: string;
unsetEntries: [string, EnvVarInfo][];
@ -603,6 +641,7 @@ function CollapsibleUnset({
onClear: (key: string) => void;
onReveal: (key: string) => void;
onCancelEdit: (key: string) => void;
clearDialogOpen?: boolean;
}) {
const [collapsed, setCollapsed] = useState(true);
const { t } = useI18n();
@ -625,6 +664,7 @@ function CollapsibleUnset({
key={key} varKey={key} info={info}
edits={edits} setEdits={setEdits} revealed={revealed} saving={saving}
onSave={onSave} onClear={onClear} onReveal={onReveal} onCancelEdit={onCancelEdit}
clearDialogOpen={clearDialogOpen}
/>
))}
</>