- Import useTheme, THEME_META, type Theme from ThemeContext - Add ORDERED_THEMES constant with three theme IDs - Add theme picker section as first section in General Settings - Color swatches use inline backgroundColor (hardcoded hex, not CSS vars) - Active theme highlighted with border-primary bg-primary/10
139 lines
5.2 KiB
TypeScript
139 lines
5.2 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
import { SlidersHorizontal } from "lucide-react";
|
|
import { instanceSettingsApi } from "@/api/instanceSettings";
|
|
import { useBreadcrumbs } from "../context/BreadcrumbContext";
|
|
import { useTheme, THEME_META, type Theme } from "../context/ThemeContext";
|
|
import { queryKeys } from "../lib/queryKeys";
|
|
import { cn } from "../lib/utils";
|
|
|
|
const ORDERED_THEMES: Theme[] = ["catppuccin-mocha", "tokyo-night", "catppuccin-latte"];
|
|
|
|
export function InstanceGeneralSettings() {
|
|
const { setBreadcrumbs } = useBreadcrumbs();
|
|
const queryClient = useQueryClient();
|
|
const [actionError, setActionError] = useState<string | null>(null);
|
|
const { theme, setTheme } = useTheme();
|
|
|
|
useEffect(() => {
|
|
setBreadcrumbs([
|
|
{ label: "Instance Settings" },
|
|
{ label: "General" },
|
|
]);
|
|
}, [setBreadcrumbs]);
|
|
|
|
const generalQuery = useQuery({
|
|
queryKey: queryKeys.instance.generalSettings,
|
|
queryFn: () => instanceSettingsApi.getGeneral(),
|
|
});
|
|
|
|
const toggleMutation = useMutation({
|
|
mutationFn: async (enabled: boolean) =>
|
|
instanceSettingsApi.updateGeneral({ censorUsernameInLogs: enabled }),
|
|
onSuccess: async () => {
|
|
setActionError(null);
|
|
await queryClient.invalidateQueries({ queryKey: queryKeys.instance.generalSettings });
|
|
},
|
|
onError: (error) => {
|
|
setActionError(error instanceof Error ? error.message : "Failed to update general settings.");
|
|
},
|
|
});
|
|
|
|
if (generalQuery.isLoading) {
|
|
return <div className="text-sm text-muted-foreground">Loading general settings...</div>;
|
|
}
|
|
|
|
if (generalQuery.error) {
|
|
return (
|
|
<div className="text-sm text-destructive">
|
|
{generalQuery.error instanceof Error
|
|
? generalQuery.error.message
|
|
: "Failed to load general settings."}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const censorUsernameInLogs = generalQuery.data?.censorUsernameInLogs === true;
|
|
|
|
return (
|
|
<div className="max-w-4xl space-y-6">
|
|
<div className="space-y-2">
|
|
<div className="flex items-center gap-2">
|
|
<SlidersHorizontal className="h-5 w-5 text-muted-foreground" />
|
|
<h1 className="text-lg font-semibold">General</h1>
|
|
</div>
|
|
<p className="text-sm text-muted-foreground">
|
|
Configure instance-wide defaults that affect how operator-visible logs are displayed.
|
|
</p>
|
|
</div>
|
|
|
|
{actionError && (
|
|
<div className="rounded-md border border-destructive/40 bg-destructive/5 px-3 py-2 text-sm text-destructive">
|
|
{actionError}
|
|
</div>
|
|
)}
|
|
|
|
<section className="rounded-xl border border-border bg-card p-5">
|
|
<div className="space-y-1.5">
|
|
<h2 className="text-sm font-semibold">Theme</h2>
|
|
<p className="text-sm text-muted-foreground">Choose the visual theme for Nexus.</p>
|
|
</div>
|
|
<div className="mt-4 flex flex-wrap gap-3">
|
|
{ORDERED_THEMES.map((id) => {
|
|
const meta = THEME_META[id];
|
|
return (
|
|
<button
|
|
key={id}
|
|
type="button"
|
|
onClick={() => setTheme(id)}
|
|
className={cn(
|
|
"flex items-center gap-2.5 rounded-lg border px-3 py-2 text-sm transition-colors",
|
|
theme === id
|
|
? "border-primary bg-primary/10 text-primary"
|
|
: "border-border bg-card text-foreground hover:border-ring",
|
|
)}
|
|
>
|
|
<span
|
|
className="h-4 w-4 rounded-full border border-border/50 shrink-0"
|
|
style={{ backgroundColor: meta.primary }}
|
|
/>
|
|
{meta.label}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</section>
|
|
|
|
<section className="rounded-xl border border-border bg-card p-5">
|
|
<div className="flex items-start justify-between gap-4">
|
|
<div className="space-y-1.5">
|
|
<h2 className="text-sm font-semibold">Censor username in logs</h2>
|
|
<p className="max-w-2xl text-sm text-muted-foreground">
|
|
Hide the username segment in home-directory paths and similar operator-visible log output. Standalone
|
|
username mentions outside of paths are not yet masked in the live transcript view. This is off by
|
|
default.
|
|
</p>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
data-slot="toggle"
|
|
aria-label="Toggle username log censoring"
|
|
disabled={toggleMutation.isPending}
|
|
className={cn(
|
|
"relative inline-flex h-5 w-9 items-center rounded-full transition-colors disabled:cursor-not-allowed disabled:opacity-60",
|
|
censorUsernameInLogs ? "bg-green-600" : "bg-muted",
|
|
)}
|
|
onClick={() => toggleMutation.mutate(!censorUsernameInLogs)}
|
|
>
|
|
<span
|
|
className={cn(
|
|
"inline-block h-3.5 w-3.5 rounded-full bg-white transition-transform",
|
|
censorUsernameInLogs ? "translate-x-4.5" : "translate-x-0.5",
|
|
)}
|
|
/>
|
|
</button>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
);
|
|
}
|