Squashed 'sample-front-end/' changes from 0881bf1..601ffe4
601ffe4 fix: Refine UI layout and visual elements 4689aaa feat: Add request timeout and cancellation to API calls git-subtree-dir: sample-front-end git-subtree-split: 601ffe468a78955839eef6c839314d9b96ea204d
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { PlexConnectionSettings, PlexServerConnection, PlexLibrary } from '../types';
|
||||
import { apiService } from '../services/api';
|
||||
import { X, Server, Lock, User, Key, Globe, Eye, EyeOff, CheckCircle, Library, ChevronDown } from 'lucide-react';
|
||||
import { X, Server, Lock, User, Key, Globe, Eye, EyeOff, CheckCircle, Library, ChevronDown, ChevronRight, Settings, Loader2 } from 'lucide-react';
|
||||
|
||||
interface ConnectionModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -18,18 +18,22 @@ const ConnectionModal: React.FC<ConnectionModalProps> = ({ isOpen, onClose, onCo
|
||||
port: '32400',
|
||||
token: '',
|
||||
username: '',
|
||||
password: ''
|
||||
password: '',
|
||||
timeout: 9
|
||||
});
|
||||
|
||||
const [isConnecting, setIsConnecting] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [showAdvanced, setShowAdvanced] = useState(false);
|
||||
|
||||
// Post-connection state
|
||||
const [connectedServerInfo, setConnectedServerInfo] = useState<PlexServerConnection | null>(null);
|
||||
const [libraries, setLibraries] = useState<PlexLibrary[]>([]);
|
||||
const [selectedLibraryId, setSelectedLibraryId] = useState<string>('');
|
||||
|
||||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
|
||||
// Reset state when opening
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
@@ -38,6 +42,12 @@ const ConnectionModal: React.FC<ConnectionModalProps> = ({ isOpen, onClose, onCo
|
||||
setLibraries([]);
|
||||
setSelectedLibraryId('');
|
||||
}
|
||||
return () => {
|
||||
// Cleanup any pending request if modal closes
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort();
|
||||
}
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
@@ -47,6 +57,11 @@ const ConnectionModal: React.FC<ConnectionModalProps> = ({ isOpen, onClose, onCo
|
||||
setFormData(prev => ({ ...prev, [name]: value }));
|
||||
};
|
||||
|
||||
const handleTimeoutChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const val = parseInt(e.target.value) || 0;
|
||||
setFormData(prev => ({ ...prev, timeout: val }));
|
||||
};
|
||||
|
||||
const handleLibraryChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const newId = e.target.value;
|
||||
setSelectedLibraryId(newId);
|
||||
@@ -55,31 +70,46 @@ const ConnectionModal: React.FC<ConnectionModalProps> = ({ isOpen, onClose, onCo
|
||||
if (lib && connectedServerInfo) {
|
||||
const updatedInfo = { ...connectedServerInfo, libraryName: lib.title };
|
||||
setConnectedServerInfo(updatedInfo);
|
||||
// Notify parent of update
|
||||
onConnectSuccess(updatedInfo);
|
||||
// Show toast
|
||||
onShowMessage(`Library switched to ${lib.title}`);
|
||||
}
|
||||
};
|
||||
|
||||
const isTokenProvided = formData.token.trim().length > 0;
|
||||
|
||||
// Dynamic styles for disabled fields
|
||||
const disabledInputClass = isTokenProvided
|
||||
? "bg-gray-700/50 text-gray-500 line-through decoration-gray-500 cursor-not-allowed border-gray-700"
|
||||
: "bg-gray-800 text-gray-100 border-gray-600 focus:border-plex-orange focus:ring-1 focus:ring-plex-orange";
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
// If already connecting, this acts as Cancel
|
||||
if (isConnecting) {
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort();
|
||||
abortControllerRef.current = null;
|
||||
setIsConnecting(false);
|
||||
setError("Connection cancelled by user.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
setError(null);
|
||||
setIsConnecting(true);
|
||||
|
||||
const result = await apiService.connectToPlex(formData);
|
||||
const abortController = new AbortController();
|
||||
abortControllerRef.current = abortController;
|
||||
|
||||
const result = await apiService.connectToPlex(formData, abortController.signal);
|
||||
|
||||
// Only proceed if we weren't aborted/cancelled (though apiService handles error msg)
|
||||
if (abortController.signal.aborted) return;
|
||||
|
||||
setIsConnecting(false);
|
||||
abortControllerRef.current = null;
|
||||
|
||||
if (result.status === 'success' && result.data) {
|
||||
// Update Token field and clear user/pass
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
token: result.data.token,
|
||||
@@ -89,17 +119,13 @@ const ConnectionModal: React.FC<ConnectionModalProps> = ({ isOpen, onClose, onCo
|
||||
|
||||
const info = result.data.serverInfo;
|
||||
setConnectedServerInfo(info);
|
||||
|
||||
// Explicitly show connection message here
|
||||
onShowMessage(`Successfully connected to ${info.name || 'Plex Server'}`);
|
||||
|
||||
// Handle libraries
|
||||
const libs = info.libraries || [];
|
||||
setLibraries(libs);
|
||||
if (libs.length > 0) {
|
||||
const defaultLib = libs[0];
|
||||
setSelectedLibraryId(defaultLib.id);
|
||||
// Pass connection info back with default library name explicitly set (though mock already does it)
|
||||
onConnectSuccess({
|
||||
...info,
|
||||
libraryName: defaultLib.title
|
||||
@@ -116,10 +142,10 @@ const ConnectionModal: React.FC<ConnectionModalProps> = ({ isOpen, onClose, onCo
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm">
|
||||
<div className="bg-gray-900 border border-gray-700 rounded-xl shadow-2xl w-full max-w-md overflow-hidden animate-in fade-in zoom-in duration-200">
|
||||
<div className="bg-gray-900 border border-gray-700 rounded-xl shadow-2xl w-full max-w-md overflow-hidden animate-in fade-in zoom-in duration-200 flex flex-col max-h-[90vh]">
|
||||
|
||||
{/* Header */}
|
||||
<div className="px-6 py-4 bg-gray-800 border-b border-gray-700 flex items-center justify-between">
|
||||
<div className="px-6 py-4 bg-gray-800 border-b border-gray-700 flex items-center justify-between flex-none">
|
||||
<h3 className="text-lg font-semibold text-white flex items-center gap-2">
|
||||
<Server size={18} className={isConnected ? "text-green-400" : "text-plex-orange"} />
|
||||
{isConnected ? 'Server Connected' : 'Connect Plex Server'}
|
||||
@@ -130,7 +156,7 @@ const ConnectionModal: React.FC<ConnectionModalProps> = ({ isOpen, onClose, onCo
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
<div className="p-6">
|
||||
<div className="p-6 overflow-y-auto custom-scrollbar">
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
|
||||
{error && (
|
||||
@@ -148,7 +174,7 @@ const ConnectionModal: React.FC<ConnectionModalProps> = ({ isOpen, onClose, onCo
|
||||
name="protocol"
|
||||
value={formData.protocol}
|
||||
onChange={handleChange}
|
||||
disabled={isConnected}
|
||||
disabled={isConnected || isConnecting}
|
||||
className={`w-full h-10 px-2 bg-gray-800 border border-gray-600 rounded-md text-sm text-white focus:border-plex-orange focus:outline-none ${isConnected ? 'opacity-60 cursor-not-allowed' : ''}`}
|
||||
>
|
||||
<option value="http">HTTP</option>
|
||||
@@ -164,7 +190,7 @@ const ConnectionModal: React.FC<ConnectionModalProps> = ({ isOpen, onClose, onCo
|
||||
type="text"
|
||||
name="address"
|
||||
required
|
||||
disabled={isConnected}
|
||||
disabled={isConnected || isConnecting}
|
||||
placeholder="IP Address or Domain"
|
||||
value={formData.address}
|
||||
onChange={handleChange}
|
||||
@@ -178,7 +204,7 @@ const ConnectionModal: React.FC<ConnectionModalProps> = ({ isOpen, onClose, onCo
|
||||
<input
|
||||
type="text"
|
||||
name="port"
|
||||
disabled={isConnected}
|
||||
disabled={isConnected || isConnecting}
|
||||
placeholder="Port (e.g. 32400)"
|
||||
value={formData.port}
|
||||
onChange={handleChange}
|
||||
@@ -201,7 +227,7 @@ const ConnectionModal: React.FC<ConnectionModalProps> = ({ isOpen, onClose, onCo
|
||||
<input
|
||||
type="text"
|
||||
name="token"
|
||||
disabled={isConnected}
|
||||
disabled={isConnected || isConnecting}
|
||||
placeholder="X-Plex-Token (Optional)"
|
||||
value={formData.token}
|
||||
onChange={handleChange}
|
||||
@@ -223,7 +249,7 @@ const ConnectionModal: React.FC<ConnectionModalProps> = ({ isOpen, onClose, onCo
|
||||
<input
|
||||
type="text"
|
||||
name="username"
|
||||
disabled={isTokenProvided}
|
||||
disabled={isTokenProvided || isConnecting}
|
||||
placeholder="Username / Email"
|
||||
value={formData.username}
|
||||
onChange={handleChange}
|
||||
@@ -239,7 +265,7 @@ const ConnectionModal: React.FC<ConnectionModalProps> = ({ isOpen, onClose, onCo
|
||||
<input
|
||||
type={showPassword ? "text" : "password"}
|
||||
name="password"
|
||||
disabled={isTokenProvided}
|
||||
disabled={isTokenProvided || isConnecting}
|
||||
placeholder="Password"
|
||||
value={formData.password}
|
||||
onChange={handleChange}
|
||||
@@ -247,33 +273,67 @@ const ConnectionModal: React.FC<ConnectionModalProps> = ({ isOpen, onClose, onCo
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
disabled={isTokenProvided}
|
||||
disabled={isTokenProvided || isConnecting}
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className={`absolute inset-y-0 right-0 pr-3 flex items-center ${isTokenProvided ? 'cursor-not-allowed opacity-50' : 'cursor-pointer text-gray-400 hover:text-white'}`}
|
||||
>
|
||||
{showPassword ? <EyeOff size={14} /> : <Eye size={14} />}
|
||||
</button>
|
||||
</div>
|
||||
{isTokenProvided && (
|
||||
<p className="text-[10px] text-yellow-500/80 italic text-center">
|
||||
Credential login disabled when token is present.
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Advanced Options */}
|
||||
{!isConnected && (
|
||||
<div className="border border-gray-800 rounded-lg overflow-hidden">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowAdvanced(!showAdvanced)}
|
||||
className="w-full flex items-center justify-between px-3 py-2 bg-gray-800/50 hover:bg-gray-800 text-xs font-medium text-gray-400 hover:text-gray-200 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Settings size={14} />
|
||||
<span>Advanced Options</span>
|
||||
</div>
|
||||
{showAdvanced ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
||||
</button>
|
||||
|
||||
{showAdvanced && (
|
||||
<div className="p-3 bg-gray-900/50 space-y-3 animate-in slide-in-from-top-2">
|
||||
<div>
|
||||
<label className="text-xs text-gray-500 mb-1 block">Connection Timeout (Seconds)</label>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max="60"
|
||||
name="timeout"
|
||||
value={formData.timeout || 9}
|
||||
onChange={handleTimeoutChange}
|
||||
disabled={isConnecting}
|
||||
className="w-full h-8 px-2 bg-gray-800 border border-gray-700 rounded-md text-xs text-white focus:border-plex-orange focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isConnected ? (
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isConnecting}
|
||||
className={`w-full mt-4 py-2.5 rounded-lg text-sm font-bold text-gray-900 transition-all shadow-lg
|
||||
className={`w-full mt-4 py-2.5 rounded-lg text-sm font-bold text-gray-900 transition-all shadow-lg flex items-center justify-center gap-2
|
||||
${isConnecting
|
||||
? 'bg-gray-600 cursor-wait'
|
||||
? 'bg-red-500/80 hover:bg-red-500 text-white animate-pulse'
|
||||
: 'bg-plex-orange hover:bg-yellow-500 active:scale-[0.98] shadow-plex-orange/20'
|
||||
}`}
|
||||
>
|
||||
{isConnecting ? 'Connecting...' : 'Connect Server'}
|
||||
{isConnecting ? (
|
||||
<>
|
||||
<Loader2 size={16} className="animate-spin" />
|
||||
<span>Connecting... <span className="opacity-75 font-normal ml-1">(Cancel)</span></span>
|
||||
</>
|
||||
) : 'Connect Server'}
|
||||
</button>
|
||||
) : (
|
||||
<div className="mt-2 p-2 bg-green-500/10 border border-green-500/20 rounded-lg text-center">
|
||||
|
||||
Reference in New Issue
Block a user