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:
parent
b436d1dabc
commit
d231073fe0
@ -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>
|
||||
|
||||
@ -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
|
||||
await loadMusicFiles();
|
||||
// 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">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user