mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-05 05:40:10 +00:00
Add backend unit test framework and modularize settings UI
Introduced a Vitest-based backend unit testing framework with supporting scripts, helpers, and GitHub Actions integration. Refactored the admin settings page to a modular architecture, splitting monolithic logic into feature-specific tabs and hooks for improved maintainability and testability. Updated documentation to reflect the new testing setup and settings architecture, and added new dependencies for testing utilities.
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
/**
|
||||
* Component: Indexers Settings Tab
|
||||
* Documentation: documentation/settings-pages.md
|
||||
*/
|
||||
|
||||
'use client';
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { IndexerManagement } from '@/components/admin/indexers/IndexerManagement';
|
||||
import { FlagConfigRow } from '@/components/admin/FlagConfigRow';
|
||||
import { IndexerFlagConfig } from '@/lib/utils/ranking-algorithm';
|
||||
import { useIndexersSettings } from './useIndexersSettings';
|
||||
import type { Settings, SavedIndexerConfig } from '../../lib/types';
|
||||
|
||||
interface IndexersTabProps {
|
||||
settings: Settings;
|
||||
indexers: SavedIndexerConfig[];
|
||||
flagConfigs: IndexerFlagConfig[];
|
||||
onChange: (settings: Settings) => void;
|
||||
onIndexersChange: (indexers: SavedIndexerConfig[]) => void;
|
||||
onFlagConfigsChange: (configs: IndexerFlagConfig[]) => void;
|
||||
onValidationChange: (isValid: boolean) => void;
|
||||
onRefreshIndexers?: () => Promise<void>;
|
||||
}
|
||||
|
||||
export function IndexersTab({
|
||||
settings,
|
||||
indexers,
|
||||
flagConfigs,
|
||||
onChange,
|
||||
onIndexersChange,
|
||||
onFlagConfigsChange,
|
||||
onValidationChange,
|
||||
onRefreshIndexers,
|
||||
}: IndexersTabProps) {
|
||||
const { testing, testResult, testConnection } = useIndexersSettings({
|
||||
prowlarrUrl: settings.prowlarr.url,
|
||||
prowlarrApiKey: settings.prowlarr.apiKey,
|
||||
onValidationChange,
|
||||
onRefreshIndexers,
|
||||
});
|
||||
|
||||
// Auto-load indexers when component mounts if prowlarr is configured
|
||||
useEffect(() => {
|
||||
if (settings.prowlarr.url && settings.prowlarr.apiKey && onRefreshIndexers) {
|
||||
onRefreshIndexers();
|
||||
}
|
||||
// Only run on mount, not when settings change
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
return (
|
||||
<div className="space-y-6 max-w-4xl">
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100 mb-4">
|
||||
Indexer Configuration
|
||||
</h2>
|
||||
<p className="text-gray-600 dark:text-gray-400 mb-6">
|
||||
Configure your Prowlarr connection and manage which indexers to use with priority and seeding time.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Prowlarr Server URL
|
||||
</label>
|
||||
<Input
|
||||
type="url"
|
||||
value={settings.prowlarr.url}
|
||||
onChange={(e) => {
|
||||
onChange({
|
||||
...settings,
|
||||
prowlarr: { ...settings.prowlarr, url: e.target.value },
|
||||
});
|
||||
onValidationChange(false);
|
||||
}}
|
||||
placeholder="http://localhost:9696"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Prowlarr API Key
|
||||
</label>
|
||||
<Input
|
||||
type="password"
|
||||
value={settings.prowlarr.apiKey}
|
||||
onChange={(e) => {
|
||||
onChange({
|
||||
...settings,
|
||||
prowlarr: { ...settings.prowlarr, apiKey: e.target.value },
|
||||
});
|
||||
onValidationChange(false);
|
||||
}}
|
||||
placeholder="Enter API key"
|
||||
/>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||
Found in Prowlarr Settings → General → Security → API Key
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-gray-200 dark:border-gray-700 pt-6">
|
||||
<Button
|
||||
onClick={testConnection}
|
||||
loading={testing}
|
||||
disabled={!settings.prowlarr.url || !settings.prowlarr.apiKey}
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
>
|
||||
Test Connection
|
||||
</Button>
|
||||
{testResult && (
|
||||
<div className={`mt-3 p-3 rounded-lg text-sm ${
|
||||
testResult.success
|
||||
? 'bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 text-green-800 dark:text-green-200'
|
||||
: 'bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 text-red-800 dark:text-red-200'
|
||||
}`}>
|
||||
{testResult.message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="border-t border-gray-200 dark:border-gray-700 pt-6">
|
||||
<IndexerManagement
|
||||
prowlarrUrl={settings.prowlarr.url}
|
||||
prowlarrApiKey={settings.prowlarr.apiKey}
|
||||
mode="settings"
|
||||
initialIndexers={indexers}
|
||||
onIndexersChange={onIndexersChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Flag Configuration Section */}
|
||||
<div className="border-t border-gray-200 dark:border-gray-700 pt-6">
|
||||
<div className="mb-4">
|
||||
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">
|
||||
Indexer Flag Configuration (Optional)
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Configure score bonuses or penalties for indexer flags like "Freeleech".
|
||||
These modifiers apply universally across all indexers and affect final torrent ranking.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{flagConfigs.length > 0 && (
|
||||
<div className="space-y-3 mb-4">
|
||||
{flagConfigs.map((config, index) => (
|
||||
<FlagConfigRow
|
||||
key={index}
|
||||
config={config}
|
||||
onChange={(updated) => {
|
||||
const newConfigs = [...flagConfigs];
|
||||
newConfigs[index] = updated;
|
||||
onFlagConfigsChange(newConfigs);
|
||||
}}
|
||||
onRemove={() => {
|
||||
onFlagConfigsChange(flagConfigs.filter((_, i) => i !== index));
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
onClick={() => {
|
||||
onFlagConfigsChange([...flagConfigs, { name: '', modifier: 0 }]);
|
||||
}}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
>
|
||||
+ Add Flag Rule
|
||||
</Button>
|
||||
|
||||
{flagConfigs.length === 0 && (
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-3 italic">
|
||||
No flag rules configured. Flag bonuses/penalties are optional.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user