feat(frontend/config): UX improvements

- Disable 'Sync S3' buttons when no S3 config present; add hint
- Remember active config tab via localStorage; enable isLazy Tabs
- Lazy-load Music Library data only when its tab is opened
- Clarify 'Auto-Link' as a cleanup action; disable when nothing to link
- Limit matching tab API calls with ?limit to reduce payloads
This commit is contained in:
Geert Rademakes 2025-08-07 23:41:33 +02:00
parent b436d1dabc
commit d231073fe0
2 changed files with 79 additions and 32 deletions

View File

@ -11,10 +11,6 @@ import {
CardHeader,
Badge,
Progress,
Alert,
AlertIcon,
AlertTitle,
AlertDescription,
useToast,
Modal,
ModalOverlay,
@ -57,7 +53,6 @@ export const SongMatching: React.FC = () => {
const [stats, setStats] = useState<MatchingStats | null>(null);
const [unmatchedMusicFiles, setUnmatchedMusicFiles] = useState<any[]>([]);
const [matchedMusicFiles, setMatchedMusicFiles] = useState<any[]>([]);
const [songsWithoutMusicFiles, setSongsWithoutMusicFiles] = useState<any[]>([]);
const [songsWithMusicFiles, setSongsWithMusicFiles] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [autoLinking, setAutoLinking] = useState(false);
@ -75,12 +70,11 @@ export const SongMatching: React.FC = () => {
const loadData = async () => {
setIsLoading(true);
try {
const [statsRes, unmatchedRes, matchedRes, songsWithoutRes, songsWithRes] = await Promise.all([
const [statsRes, unmatchedRes, matchedRes, songsWithRes] = await Promise.all([
fetch('/api/matching/stats'),
fetch('/api/matching/unmatched-music-files'),
fetch('/api/matching/matched-music-files'),
fetch('/api/matching/songs-without-music-files'),
fetch('/api/matching/songs-with-music-files')
fetch('/api/matching/unmatched-music-files?limit=200'),
fetch('/api/matching/matched-music-files?limit=200'),
fetch('/api/matching/songs-with-music-files?limit=200')
]);
if (statsRes.ok) {
@ -98,11 +92,6 @@ export const SongMatching: React.FC = () => {
setMatchedMusicFiles(matchedData.musicFiles);
}
if (songsWithoutRes.ok) {
const songsData = await songsWithoutRes.json();
setSongsWithoutMusicFiles(songsData.songs);
}
if (songsWithRes.ok) {
const songsData = await songsWithRes.json();
setSongsWithMusicFiles(songsData.songs);
@ -257,12 +246,7 @@ export const SongMatching: React.FC = () => {
return 'red';
};
const formatDuration = (seconds: number): string => {
if (!seconds || isNaN(seconds)) return '00:00';
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
};
const canAutoLink = !!stats && stats.unmatchedMusicFiles > 0;
if (isLoading) {
return (
@ -315,7 +299,7 @@ export const SongMatching: React.FC = () => {
<CardBody>
<VStack spacing={4}>
<Text color="gray.300" textAlign="center">
Automatically match and link music files to songs in your Rekordbox library
Try linking any remaining unmatched files. The main S3 sync already performs matching; use this for leftovers.
</Text>
<Button
leftIcon={<FiZap />}
@ -325,8 +309,9 @@ export const SongMatching: React.FC = () => {
isLoading={autoLinking}
loadingText="Auto-Linking..."
_hover={{ bg: "blue.700" }}
isDisabled={autoLinking || !canAutoLink}
>
Auto-Link Files
Auto-Link Remaining Files
</Button>
</VStack>
</CardBody>

View File

@ -35,7 +35,7 @@ import { S3Configuration } from "./S3Configuration";
import { MusicUpload } from "../components/MusicUpload";
import { SongMatching } from "../components/SongMatching";
import { api } from "../services/api";
import { useState, useEffect } from "react";
import { useState, useEffect, useMemo } from "react";
interface MusicFile {
_id: string;
@ -60,11 +60,52 @@ export function Configuration() {
const [musicFiles, setMusicFiles] = useState<MusicFile[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [isSyncing, setIsSyncing] = useState(false);
const [hasS3Config, setHasS3Config] = useState<boolean>(false);
const [musicLoaded, setMusicLoaded] = useState<boolean>(false);
// Load music files on component mount
useEffect(() => {
loadMusicFiles();
// Tabs: remember active tab across refreshes
const initialTabIndex = useMemo(() => {
const stored = localStorage.getItem('configTabIndex');
return stored ? parseInt(stored, 10) : 0;
}, []);
const [tabIndex, setTabIndex] = useState<number>(initialTabIndex);
// Tab indices (keep in sync with Tab order below)
const TAB_INDEX = {
LIBRARY: 0,
UPLOAD: 1,
MUSIC_LIBRARY: 2,
MATCHING: 3,
S3_CONFIG: 4,
} as const;
// Fetch S3 config (small and safe to do on mount)
useEffect(() => {
const fetchS3Config = async () => {
try {
const res = await fetch('/api/config/s3');
if (res.ok) {
const data = await res.json();
const cfg = data?.config || {};
const required = ['endpoint', 'region', 'accessKeyId', 'secretAccessKey', 'bucketName'];
const present = required.every((k) => typeof cfg[k] === 'string' && cfg[k]);
setHasS3Config(present);
} else {
setHasS3Config(false);
}
} catch {
setHasS3Config(false);
}
};
fetchS3Config();
}, []);
// Lazy-load: only load music files when Music Library tab becomes active
useEffect(() => {
if (tabIndex === TAB_INDEX.MUSIC_LIBRARY && !musicLoaded) {
loadMusicFiles().then(() => setMusicLoaded(true));
}
}, [tabIndex]);
const loadMusicFiles = async () => {
setIsLoading(true);
@ -107,8 +148,13 @@ export function Configuration() {
isClosable: true,
});
// Reload music files to show the new ones
// Reload music files to show the new ones if user is on Music Library tab
if (tabIndex === TAB_INDEX.MUSIC_LIBRARY) {
await loadMusicFiles();
} else {
// Mark as not loaded so that when user opens the tab, it fetches fresh
setMusicLoaded(false);
}
} else {
throw new Error('Failed to sync S3');
}
@ -241,7 +287,16 @@ export function Configuration() {
<Heading size="lg">Configuration</Heading>
</Flex>
<Tabs variant="enclosed" colorScheme="blue">
<Tabs
variant="enclosed"
colorScheme="blue"
isLazy
index={tabIndex}
onChange={(index) => {
setTabIndex(index);
localStorage.setItem('configTabIndex', String(index));
}}
>
<TabList bg="gray.800" borderColor="gray.700">
<Tab color="gray.300" _selected={{ bg: "gray.700", color: "white", borderColor: "gray.600" }}>
<HStack spacing={2}>
@ -351,6 +406,7 @@ export function Configuration() {
colorScheme="blue"
onClick={handleSyncS3}
isLoading={isSyncing}
isDisabled={!hasS3Config || isSyncing}
loadingText="Syncing..."
_hover={{ bg: "blue.900", borderColor: "blue.400" }}
>
@ -359,11 +415,11 @@ export function Configuration() {
</HStack>
</HStack>
{isLoading ? (
{tabIndex === TAB_INDEX.MUSIC_LIBRARY && isLoading ? (
<Text textAlign="center" color="gray.500">
Loading music files...
</Text>
) : musicFiles.length === 0 ? (
) : tabIndex === TAB_INDEX.MUSIC_LIBRARY && musicFiles.length === 0 ? (
<VStack spacing={4} textAlign="center" color="gray.500">
<Text>No music files found in the database.</Text>
<Text fontSize="sm">
@ -373,12 +429,18 @@ export function Configuration() {
leftIcon={<FiRefreshCw />}
onClick={handleSyncS3}
isLoading={isSyncing}
isDisabled={!hasS3Config || isSyncing}
loadingText="Syncing..."
colorScheme="blue"
_hover={{ bg: "blue.700" }}
>
Sync S3 Bucket
</Button>
{!hasS3Config && (
<Text fontSize="xs" color="orange.300">
Configure your S3 connection in the S3 Configuration tab first.
</Text>
)}
</VStack>
) : (
<VStack spacing={2} align="stretch">