fix: config page
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm, Controller } from "react-hook-form";
|
||||
import { authApiClient } from "../../lib/api-client";
|
||||
import { toast } from "sonner";
|
||||
@@ -16,12 +16,32 @@ interface WebhookSettings {
|
||||
available_events: string[]; // Provided by API, not saved
|
||||
}
|
||||
|
||||
// --- API Functions ---
|
||||
const fetchSpotifyApiConfig = async (): Promise<SpotifyApiSettings> => {
|
||||
const { data } = await authApiClient.client.get("/credentials/spotify_api_config");
|
||||
return data;
|
||||
interface ServerConfig {
|
||||
client_id?: string;
|
||||
client_secret?: string;
|
||||
utilityConcurrency?: number;
|
||||
librespotConcurrency?: number;
|
||||
url?: string;
|
||||
events?: string[];
|
||||
}
|
||||
|
||||
const fetchServerConfig = async (): Promise<ServerConfig> => {
|
||||
const [spotifyConfig, generalConfig] = await Promise.all([
|
||||
authApiClient.client.get("/credentials/spotify_api_config").catch(() => ({ data: {} })),
|
||||
authApiClient.getConfig<any>(),
|
||||
]);
|
||||
|
||||
return {
|
||||
...spotifyConfig.data,
|
||||
...generalConfig,
|
||||
};
|
||||
};
|
||||
|
||||
const saveServerConfig = async (data: Partial<ServerConfig>) => {
|
||||
const payload = { ...data };
|
||||
const { data: response } = await authApiClient.client.post("/config", payload);
|
||||
return response;
|
||||
};
|
||||
const saveSpotifyApiConfig = (data: SpotifyApiSettings) => authApiClient.client.put("/credentials/spotify_api_config", data);
|
||||
|
||||
const fetchWebhookConfig = async (): Promise<WebhookSettings> => {
|
||||
// Mock a response since backend endpoint doesn't exist
|
||||
@@ -32,40 +52,34 @@ const fetchWebhookConfig = async (): Promise<WebhookSettings> => {
|
||||
available_events: ["download_start", "download_complete", "download_failed", "watch_added"],
|
||||
});
|
||||
};
|
||||
const saveWebhookConfig = (data: Partial<WebhookSettings>) => {
|
||||
toast.info("Webhook configuration is not available.");
|
||||
return Promise.resolve(data);
|
||||
|
||||
const saveWebhookConfig = async (data: Partial<WebhookSettings>) => {
|
||||
const payload = { ...data };
|
||||
const { data: response } = await authApiClient.client.post("/config", payload);
|
||||
return response;
|
||||
};
|
||||
|
||||
const testWebhook = (url: string) => {
|
||||
toast.info("Webhook testing is not available.");
|
||||
return Promise.resolve(url);
|
||||
};
|
||||
|
||||
// --- Components ---
|
||||
function SpotifyApiForm() {
|
||||
const queryClient = useQueryClient();
|
||||
const { data, isLoading } = useQuery({ queryKey: ["spotifyApiConfig"], queryFn: fetchSpotifyApiConfig });
|
||||
function SpotifyApiForm({ config, onConfigChange }: { config: ServerConfig; onConfigChange: (updates: Partial<ServerConfig>) => void }) {
|
||||
const { register, handleSubmit, reset } = useForm<SpotifyApiSettings>();
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: saveSpotifyApiConfig,
|
||||
onSuccess: () => {
|
||||
toast.success("Spotify API settings saved!");
|
||||
queryClient.invalidateQueries({ queryKey: ["spotifyApiConfig"] });
|
||||
},
|
||||
onError: (e) => {
|
||||
console.error("Failed to save Spotify API settings:", (e as any).message);
|
||||
toast.error(`Failed to save: ${(e as any).message}`);
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) reset(data);
|
||||
}, [data, reset]);
|
||||
if (config) {
|
||||
reset({
|
||||
client_id: config.client_id || "",
|
||||
client_secret: config.client_secret || "",
|
||||
});
|
||||
}
|
||||
}, [config, reset]);
|
||||
|
||||
const onSubmit = (formData: SpotifyApiSettings) => mutation.mutate(formData);
|
||||
|
||||
if (isLoading) return <p className="text-content-muted dark:text-content-muted-dark">Loading Spotify API settings...</p>;
|
||||
const onSubmit = (formData: SpotifyApiSettings) => {
|
||||
onConfigChange(formData);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||
@@ -73,15 +87,10 @@ function SpotifyApiForm() {
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={mutation.isPending}
|
||||
className="px-4 py-2 bg-button-primary hover:bg-button-primary-hover text-button-primary-text rounded-md disabled:opacity-50"
|
||||
title="Save Spotify API"
|
||||
className="px-4 py-2 bg-button-primary hover:bg-button-primary-hover text-button-primary-text rounded-md"
|
||||
title="Save Spotify API Settings"
|
||||
>
|
||||
{mutation.isPending ? (
|
||||
<img src="/spinner.svg" alt="Saving" className="w-5 h-5 animate-spin logo" />
|
||||
) : (
|
||||
<img src="/save.svg" alt="Save" className="w-5 h-5 logo" />
|
||||
)}
|
||||
<img src="/save.svg" alt="Save" className="w-5 h-5 logo" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -110,54 +119,31 @@ function SpotifyApiForm() {
|
||||
);
|
||||
}
|
||||
|
||||
function UtilityConcurrencyForm() {
|
||||
const queryClient = useQueryClient();
|
||||
const { data: configData, isLoading } = useQuery({
|
||||
queryKey: ["config"],
|
||||
queryFn: () => authApiClient.getConfig<any>(),
|
||||
});
|
||||
|
||||
function UtilityConcurrencyForm({ config, onConfigChange }: { config: ServerConfig; onConfigChange: (updates: Partial<ServerConfig>) => void }) {
|
||||
const { register, handleSubmit, reset, formState: { isDirty } } = useForm<{ utilityConcurrency: number }>();
|
||||
|
||||
useEffect(() => {
|
||||
if (configData) {
|
||||
reset({ utilityConcurrency: Number(configData.utilityConcurrency ?? 1) });
|
||||
if (config) {
|
||||
reset({ utilityConcurrency: Number(config.utilityConcurrency ?? 1) });
|
||||
}
|
||||
}, [configData, reset]);
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: (payload: { utilityConcurrency: number }) => authApiClient.updateConfig(payload),
|
||||
onSuccess: () => {
|
||||
toast.success("Utility worker concurrency saved!");
|
||||
queryClient.invalidateQueries({ queryKey: ["config"] });
|
||||
},
|
||||
onError: (e) => {
|
||||
toast.error(`Failed to save: ${(e as any).message}`);
|
||||
},
|
||||
});
|
||||
}, [config, reset]);
|
||||
|
||||
const onSubmit = (values: { utilityConcurrency: number }) => {
|
||||
const value = Math.max(1, Number(values.utilityConcurrency || 1));
|
||||
mutation.mutate({ utilityConcurrency: value });
|
||||
onConfigChange({ utilityConcurrency: value });
|
||||
};
|
||||
|
||||
if (isLoading) return <p className="text-content-muted dark:text-content-muted-dark">Loading server settings...</p>;
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||
<div className="flex items-center justify-end mb-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={mutation.isPending || !isDirty}
|
||||
disabled={!isDirty}
|
||||
className="px-4 py-2 bg-button-primary hover:bg-button-primary-hover text-button-primary-text rounded-md disabled:opacity-50"
|
||||
title="Save Utility Concurrency"
|
||||
>
|
||||
{mutation.isPending ? (
|
||||
<img src="/spinner.svg" alt="Saving" className="w-5 h-5 animate-spin logo" />
|
||||
) : (
|
||||
<img src="/save.svg" alt="Save" className="w-5 h-5 logo" />
|
||||
)}
|
||||
<img src="/save.svg" alt="Save" className="w-5 h-5 logo" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -179,55 +165,32 @@ function UtilityConcurrencyForm() {
|
||||
);
|
||||
}
|
||||
|
||||
function LibrespotConcurrencyForm() {
|
||||
const queryClient = useQueryClient();
|
||||
const { data: configData, isLoading } = useQuery({
|
||||
queryKey: ["config"],
|
||||
queryFn: () => authApiClient.getConfig<any>(),
|
||||
});
|
||||
|
||||
function LibrespotConcurrencyForm({ config, onConfigChange }: { config: ServerConfig; onConfigChange: (updates: Partial<ServerConfig>) => void }) {
|
||||
const { register, handleSubmit, reset, formState: { isDirty } } = useForm<{ librespotConcurrency: number }>();
|
||||
|
||||
useEffect(() => {
|
||||
if (configData) {
|
||||
reset({ librespotConcurrency: Number(configData.librespotConcurrency ?? 2) });
|
||||
if (config) {
|
||||
reset({ librespotConcurrency: Number(config.librespotConcurrency ?? 2) });
|
||||
}
|
||||
}, [configData, reset]);
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: (payload: { librespotConcurrency: number }) => authApiClient.updateConfig(payload),
|
||||
onSuccess: () => {
|
||||
toast.success("Librespot concurrency saved!");
|
||||
queryClient.invalidateQueries({ queryKey: ["config"] });
|
||||
},
|
||||
onError: (e) => {
|
||||
toast.error(`Failed to save: ${(e as any).message}`);
|
||||
},
|
||||
});
|
||||
}, [config, reset]);
|
||||
|
||||
const onSubmit = (values: { librespotConcurrency: number }) => {
|
||||
const raw = Number(values.librespotConcurrency || 2);
|
||||
const safe = Math.max(1, Math.min(16, raw));
|
||||
mutation.mutate({ librespotConcurrency: safe });
|
||||
onConfigChange({ librespotConcurrency: safe });
|
||||
};
|
||||
|
||||
if (isLoading) return <p className="text-content-muted dark:text-content-muted-dark">Loading server settings...</p>;
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||
<div className="flex items-center justify-end mb-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={mutation.isPending || !isDirty}
|
||||
disabled={!isDirty}
|
||||
className="px-4 py-2 bg-button-primary hover:bg-button-primary-hover text-button-primary-text rounded-md disabled:opacity-50"
|
||||
title="Save Librespot Concurrency"
|
||||
>
|
||||
{mutation.isPending ? (
|
||||
<img src="/spinner.svg" alt="Saving" className="w-5 h-5 animate-spin logo" />
|
||||
) : (
|
||||
<img src="/save.svg" alt="Save" className="w-5 h-5 logo" />
|
||||
)}
|
||||
<img src="/save.svg" alt="Save" className="w-5 h-5 logo" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -293,7 +256,7 @@ function WebhookForm() {
|
||||
type="submit"
|
||||
disabled={mutation.isPending}
|
||||
className="px-4 py-2 bg-button-primary hover:bg-button-primary-hover text-button-primary-text rounded-md disabled:opacity-50"
|
||||
title="Save Webhook"
|
||||
title="Save Webhook Settings"
|
||||
>
|
||||
{mutation.isPending ? (
|
||||
<img src="/spinner.svg" alt="Saving" className="w-5 h-5 animate-spin logo" />
|
||||
@@ -356,24 +319,61 @@ function WebhookForm() {
|
||||
}
|
||||
|
||||
export function ServerTab() {
|
||||
const queryClient = useQueryClient();
|
||||
const [localConfig, setLocalConfig] = useState<ServerConfig>({});
|
||||
|
||||
const { data: serverConfig, isLoading } = useQuery({
|
||||
queryKey: ["serverConfig"],
|
||||
queryFn: fetchServerConfig,
|
||||
});
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: saveServerConfig,
|
||||
onSuccess: () => {
|
||||
toast.success("Server settings saved successfully!");
|
||||
queryClient.invalidateQueries({ queryKey: ["serverConfig"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["config"] });
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("Failed to save server settings", (error as any).message);
|
||||
toast.error(`Failed to save server settings: ${(error as any).message}`);
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (serverConfig) {
|
||||
setLocalConfig(serverConfig);
|
||||
}
|
||||
}, [serverConfig]);
|
||||
|
||||
const handleConfigChange = (updates: Partial<ServerConfig>) => {
|
||||
const newConfig = { ...localConfig, ...updates };
|
||||
setLocalConfig(newConfig);
|
||||
mutation.mutate(newConfig);
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return <div>Loading server settings...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold text-content-primary dark:text-content-primary-dark">Spotify API</h3>
|
||||
<p className="text-sm text-content-muted dark:text-content-muted-dark mt-1">Provide your own API credentials to avoid rate-limiting issues.</p>
|
||||
<SpotifyApiForm />
|
||||
<SpotifyApiForm config={localConfig} onConfigChange={handleConfigChange} />
|
||||
</div>
|
||||
<hr className="border-border dark:border-border-dark" />
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold text-content-primary dark:text-content-primary-dark">Utility Worker</h3>
|
||||
<p className="text-sm text-content-muted dark:text-content-muted-dark mt-1">Tune background utility worker concurrency for low-powered systems.</p>
|
||||
<UtilityConcurrencyForm />
|
||||
<UtilityConcurrencyForm config={localConfig} onConfigChange={handleConfigChange} />
|
||||
</div>
|
||||
<hr className="border-border dark:border-border-dark" />
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold text-content-primary dark:text-content-primary-dark">Librespot</h3>
|
||||
<p className="text-sm text-content-muted dark:text-content-muted-dark mt-1">Adjust Librespot client worker threads.</p>
|
||||
<LibrespotConcurrencyForm />
|
||||
<LibrespotConcurrencyForm config={localConfig} onConfigChange={handleConfigChange} />
|
||||
</div>
|
||||
<hr className="border-border dark:border-border-dark" />
|
||||
<div>
|
||||
|
||||
Reference in New Issue
Block a user