fix: show active qBittorrent transfers

This commit is contained in:
Developer
2026-07-14 16:07:04 +00:00
parent 70511d97f9
commit a541a4fd16
11 changed files with 376 additions and 187 deletions
+78 -83
View File
@@ -77,7 +77,12 @@ const SERVICE_OPTIONS = [
// maps to this sentinel and converts back to "" at the draft boundary.
const NONE = "__none__";
type SettingsTab = "machines" | "ssh-keys" | "services" | "danger";
type SettingsTab =
| "machines"
| "ssh-keys"
| "services"
| "dashboards"
| "danger";
/** Small labeled-field wrapper replacing the MUI `<TextField label>` shell. */
function FormField({
@@ -1018,6 +1023,9 @@ export function Settings() {
<TabsTrigger key="services" value="services">
Services
</TabsTrigger>,
<TabsTrigger key="dashboards" value="dashboards">
Dashboards
</TabsTrigger>,
<TabsTrigger key="danger" value="danger">
Danger Zone
</TabsTrigger>,
@@ -1204,6 +1212,7 @@ export function Settings() {
{tab === "services" && (
<ServicesAdminCard initialServiceId={initialServiceId} />
)}
{tab === "dashboards" && <DashboardManagementCard />}
{tab === "danger" && <ResetLocalDatabaseCard />}
</TabbedCard>
{isMobile ? (
@@ -1354,7 +1363,6 @@ function ServicesAdminCard({
const { data: services = [] } = useServiceInstances();
const { data: types = [] } = useServiceTypes();
const [selectedServiceId, setSelectedServiceId] = useState(initialServiceId);
const [serviceSubtab, setServiceSubtab] = useState<"list" | "dashboards">("list");
const [createOpen, setCreateOpen] = useState(false);
const sortedServices = useMemo(
@@ -1381,89 +1389,76 @@ function ServicesAdminCard({
return (
<>
<TabbedCard
value={serviceSubtab}
onChange={(value) => setServiceSubtab(value as "list" | "dashboards")}
tabs={[
<TabsTrigger key="list" value="list">List</TabsTrigger>,
<TabsTrigger key="dashboards" value="dashboards">Dashboards</TabsTrigger>,
]}
contentSx={{}}
>
{serviceSubtab === "list" && (
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-4">
<div className="flex justify-end">
<Button variant="outline" onClick={() => setCreateOpen(true)}>
Add service
</Button>
</div>
{sortedServices.length === 0 ? (
<p className="text-sm text-muted-foreground">
No service instances configured yet.
</p>
) : (
<div className="grid grid-cols-1 gap-4 md:grid-cols-[320px_minmax(0,1fr)]">
<SelectionRailCard
title="Services"
description="Select a service to edit its configuration."
minHeight={420}
>
{sortedServices.map((svc) => {
const active = svc.id === (selectedService?.id ?? "");
const typeName =
types.find((t) => t.service_type === svc.service_type)?.name ??
svc.service_type;
return (
<div
key={svc.id}
onClick={() => setSelectedServiceId(svc.id)}
className={cn(
"group grid w-full cursor-pointer grid-cols-[minmax(0,1fr)_auto] gap-2 border-t border-border px-3 py-2.5",
active ? "bg-muted" : "bg-card hover:bg-muted/50",
)}
>
<div className="min-w-0">
<p className="truncate font-semibold">{svc.name}</p>
<p className="text-xs text-muted-foreground">
{typeName} · {svc.enabled ? "Enabled" : "Disabled"}
</p>
<Button variant="outline" onClick={() => setCreateOpen(true)}>
Add service
</Button>
</div>
{sortedServices.length === 0 ? (
<p className="text-sm text-muted-foreground">
No service instances configured yet.
</p>
) : (
<div className="grid grid-cols-1 gap-4 md:grid-cols-[320px_minmax(0,1fr)]">
<SelectionRailCard
title="Services"
description="Select a service to edit its configuration."
minHeight={420}
>
{sortedServices.map((svc) => {
const active = svc.id === (selectedService?.id ?? "");
const typeName =
types.find((t) => t.service_type === svc.service_type)
?.name ?? svc.service_type;
return (
<div
key={svc.id}
onClick={() => setSelectedServiceId(svc.id)}
className={cn(
"group grid w-full cursor-pointer grid-cols-[minmax(0,1fr)_auto] gap-2 border-t border-border px-3 py-2.5",
active ? "bg-muted" : "bg-card hover:bg-muted/50",
)}
>
<div className="min-w-0">
<p className="truncate font-semibold">{svc.name}</p>
<p className="text-xs text-muted-foreground">
{typeName} · {svc.enabled ? "Enabled" : "Disabled"}
</p>
</div>
<Badge variant={svc.enabled ? "default" : "secondary"}>
{svc.enabled ? "on" : "off"}
</Badge>
</div>
);
})}
</SelectionRailCard>
<SectionCard
title={selectedService?.name ?? "No service selected"}
description={
selectedTypeInfo?.description ??
"Select a service on the left to edit its configuration."
}
>
{selectedService ? (
<ServiceConfigEditor
// Remount on service switch so useState initializers (name, config,
// secrets) re-run for the new instance. Without this key, switching
// services in the rail keeps the previous service's editable state
// and a save writes stale name/config onto the new service's row.
key={selectedService.id}
instance={selectedService}
typeInfo={selectedTypeInfo}
/>
) : (
<p className="text-sm text-muted-foreground">
Select a service on the left.
</p>
)}
</SectionCard>
</div>
<Badge variant={svc.enabled ? "default" : "secondary"}>
{svc.enabled ? "on" : "off"}
</Badge>
</div>
);
})}
</SelectionRailCard>
<SectionCard
title={selectedService?.name ?? "No service selected"}
description={
selectedTypeInfo?.description ??
"Select a service on the left to edit its configuration."
}
>
{selectedService ? (
<ServiceConfigEditor
// Remount on service switch so useState initializers (name, config,
// secrets) re-run for the new instance. Without this key, switching
// services in the rail keeps the previous service's editable state
// and a save writes stale name/config onto the new service's row.
key={selectedService.id}
instance={selectedService}
typeInfo={selectedTypeInfo}
/>
) : (
<p className="text-sm text-muted-foreground">
Select a service on the left.
</p>
)}
</SectionCard>
</div>
)}
</div>
)}
{serviceSubtab === "dashboards" && <DashboardManagementCard />}
</TabbedCard>
)}
</div>
<CreateServiceDialog
open={createOpen}
onClose={() => setCreateOpen(false)}