Files
profilarr/frontend/src/components/settings/arrs/ArrModal.jsx
Sam Chau 19c6be2f21 feature: save external app connections (#8)
- SQLite DB added
- Store external app connections
- Authentication, tags, type, name implemented
2025-02-05 16:09:59 +10:30

497 lines
18 KiB
JavaScript

import React, {useState, useEffect} from 'react';
import {Plus, TestTube, Loader, Save, Tag, X, Trash, Check} from 'lucide-react';
import Modal from '../../ui/Modal';
import Tooltip from '../../ui/Tooltip';
import Alert from '../../ui/Alert';
import {
pingService,
saveArrConfig,
updateArrConfig,
deleteArrConfig
} from '../../../api/arr';
const ArrModal = ({isOpen, onClose, onSubmit, editingArr}) => {
const [formData, setFormData] = useState({
name: '',
type: 'radarr',
tags: [],
profilarrServer: '',
arrServer: '',
apiKey: ''
});
const [tagInput, setTagInput] = useState('');
const [errors, setErrors] = useState({});
const [isTestingConnection, setIsTestingConnection] = useState(false);
const [deleteConfirm, setDeleteConfirm] = useState(false);
const [saveConfirm, setSaveConfirm] = useState(false);
const [testConfirm, setTestConfirm] = useState(false);
const arrTypes = [
{value: 'radarr', label: 'Radarr'},
{value: 'sonarr', label: 'Sonarr'}
];
useEffect(() => {
if (editingArr) {
setFormData(editingArr);
} else {
setFormData({
name: '',
type: '',
tags: [],
profilarrServer: '',
arrServer: '',
apiKey: ''
});
}
setTagInput('');
setErrors({});
setDeleteConfirm(false);
setSaveConfirm(false);
setTestConfirm(false);
}, [editingArr]);
const validateUrl = url => {
try {
new URL(url);
return true;
} catch {
return false;
}
};
const validateForm = () => {
const newErrors = {};
if (
formData.profilarrServer &&
!validateUrl(formData.profilarrServer)
) {
newErrors.profilarrServer =
'Please enter a valid URL (e.g., http://localhost:7441)';
}
if (formData.arrServer && !validateUrl(formData.arrServer)) {
newErrors.arrServer =
'Please enter a valid URL (e.g., http://localhost:7878)';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleTestConnection = async () => {
if (!validateForm()) return;
if (testConfirm) {
setIsTestingConnection(true);
try {
const result = await pingService(
formData.arrServer,
formData.apiKey,
formData.type // Add type
);
if (result.success) {
Alert.success('Connection successful!');
} else {
Alert.error(`Connection failed: ${result.message}`);
}
} catch (error) {
Alert.error('An error occurred while testing the connection.');
console.error('Error testing connection:', error);
} finally {
setIsTestingConnection(false);
setTestConfirm(false);
}
} else {
setTestConfirm(true);
setTimeout(() => setTestConfirm(false), 3000);
}
};
const handleSubmit = async e => {
e.preventDefault();
if (!validateForm()) return;
if (saveConfirm) {
try {
// Test connection with type verification
const testResult = await pingService(
formData.arrServer,
formData.apiKey,
formData.type // Add type
);
if (!testResult.success) {
Alert.error(
`Connection test failed: ${testResult.message}`
);
setSaveConfirm(false);
return;
}
// If connection test passed, proceed with save/update
if (editingArr) {
const result = await updateArrConfig(
editingArr.id,
formData
);
if (result.success) {
Alert.success('Configuration updated successfully');
onSubmit();
}
} else {
const result = await saveArrConfig(formData);
if (result.success) {
Alert.success('Configuration saved successfully');
onSubmit();
}
}
} catch (error) {
Alert.error('Failed to save configuration');
console.error('Error saving configuration:', error);
}
setSaveConfirm(false);
} else {
setSaveConfirm(true);
setTimeout(() => setSaveConfirm(false), 3000);
}
};
const handleDelete = async () => {
if (deleteConfirm) {
try {
const result = await deleteArrConfig(editingArr.id);
if (result.success) {
Alert.success('Configuration deleted successfully');
onSubmit();
}
} catch (error) {
Alert.error('Failed to delete configuration');
console.error('Error deleting configuration:', error);
}
setDeleteConfirm(false);
} else {
setDeleteConfirm(true);
setTimeout(() => setDeleteConfirm(false), 3000);
}
};
const handleInputChange = e => {
const {id, value} = e.target;
setFormData(prev => ({
...prev,
[id]: value
}));
if (errors[id]) {
setErrors(prev => ({
...prev,
[id]: ''
}));
}
};
const handleAddTag = e => {
e.preventDefault();
if (tagInput.trim()) {
if (!formData.tags.includes(tagInput.trim())) {
setFormData(prev => ({
...prev,
tags: [...prev.tags, tagInput.trim()]
}));
}
setTagInput('');
}
};
const handleRemoveTag = tagToRemove => {
setFormData(prev => ({
...prev,
tags: prev.tags.filter(tag => tag !== tagToRemove)
}));
};
const handleTagInputKeyDown = e => {
if (e.key === 'Enter' || e.key === ',') {
e.preventDefault();
handleAddTag(e);
}
};
const inputClasses = errorKey => `
w-full px-3 py-2 text-sm rounded-lg
border ${
errors[errorKey]
? 'border-red-500'
: 'border-gray-300 dark:border-gray-600'
}
bg-white dark:bg-gray-700
text-gray-900 dark:text-white
focus:ring-2 ${
errors[errorKey]
? 'focus:ring-red-500 focus:border-red-500'
: 'focus:ring-blue-500 focus:border-blue-500'
}
placeholder-gray-400 dark:placeholder-gray-500
transition-all
`;
return (
<Modal
isOpen={isOpen}
onClose={onClose}
title={editingArr ? 'Edit Arr' : 'Add New Arr'}
width='2xl'>
<form
onSubmit={handleSubmit}
className='space-y-4 overflow-visible'>
<div className='space-y-1.5'>
<div className='flex items-center space-x-2'>
<Tooltip content='A unique name to identify this Arr instance'>
<label
htmlFor='name'
className='text-xs font-medium text-gray-700 dark:text-gray-300 cursor-help'>
Name
</label>
</Tooltip>
</div>
<input
id='name'
value={formData.name}
onChange={handleInputChange}
className={inputClasses('name')}
placeholder='My App Instance'
required
/>
</div>
{/* Add Type Selection */}
<div className='space-y-1.5'>
<div className='flex items-center space-x-2'>
<Tooltip content='Select the type of Arr instance'>
<label
htmlFor='type'
className='text-xs font-medium text-gray-700 dark:text-gray-300 cursor-help'>
Type
</label>
</Tooltip>
</div>
<select
id='type'
value={formData.type}
onChange={handleInputChange}
className={inputClasses('type')}
required>
{arrTypes.map(type => (
<option key={type.value} value={type.value}>
{type.label}
</option>
))}
</select>
</div>
{/* Keep the rest of your existing form fields */}
<div className='space-y-1.5'>
{/* Tags section */}
<div className='flex items-center space-x-2'>
<Tooltip content='Optional tags to categorize and filter your Arr instances'>
<label
htmlFor='tags'
className='text-xs font-medium text-gray-700 dark:text-gray-300 cursor-help flex items-center'>
Tags
<Tag size={12} className='ml-1 text-gray-400' />
</label>
</Tooltip>
</div>
<div className='flex flex-wrap gap-2 mb-2'>
{formData.tags.map((tag, index) => (
<span
key={index}
className='inline-flex items-center bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300 text-xs rounded px-2 py-1'>
{tag}
<button
type='button'
onClick={() => handleRemoveTag(tag)}
className='ml-1 hover:text-blue-900 dark:hover:text-blue-200'>
<X size={12} />
</button>
</span>
))}
</div>
<div className='flex gap-2'>
<input
id='tagInput'
value={tagInput}
onChange={e => setTagInput(e.target.value)}
onKeyDown={handleTagInputKeyDown}
className={inputClasses('tagInput')}
placeholder='Enter tags...'
/>
<button
type='button'
onClick={handleAddTag}
className='px-3 py-2 text-sm rounded-lg bg-blue-100 text-blue-600 hover:bg-blue-200
dark:bg-blue-900 dark:text-blue-300 dark:hover:bg-blue-800
font-medium transition-colors'>
Add
</button>
</div>
</div>
{/* Rest of the fields */}
<div className='space-y-1.5'>
<div className='flex items-center space-x-2'>
<Tooltip content='The URL of your Profilarr server as this app would see it.(e.g., http://localhost:7441)'>
<label
htmlFor='profilarrServer'
className='text-xs font-medium text-gray-700 dark:text-gray-300 cursor-help'>
Profilarr Server
</label>
</Tooltip>
</div>
<input
id='profilarrServer'
value={formData.profilarrServer}
onChange={handleInputChange}
className={inputClasses('profilarrServer')}
placeholder='http://localhost:7441'
required
/>
{errors.profilarrServer && (
<p className='text-xs text-red-500 mt-1'>
{errors.profilarrServer}
</p>
)}
</div>
<div className='space-y-1.5'>
<div className='flex items-center space-x-2'>
<Tooltip content='The URL of this app as Profilarr would see it (e.g., http://localhost:7878 for Radarr)'>
<label
htmlFor='arrServer'
className='text-xs font-medium text-gray-700 dark:text-gray-300 cursor-help'>
Arr Server
</label>
</Tooltip>
</div>
<input
id='arrServer'
value={formData.arrServer}
onChange={handleInputChange}
className={inputClasses('arrServer')}
placeholder='http://localhost:7878'
required
/>
{errors.arrServer && (
<p className='text-xs text-red-500 mt-1'>
{errors.arrServer}
</p>
)}
</div>
<div className='space-y-1.5'>
<div className='flex items-center space-x-2'>
<Tooltip content='Your Arr instance API key. Find this in Settings > General'>
<label
htmlFor='apiKey'
className='text-xs font-medium text-gray-700 dark:text-gray-300 cursor-help'>
API Key
</label>
</Tooltip>
</div>
<input
type='password'
id='apiKey'
value={formData.apiKey}
onChange={handleInputChange}
className={inputClasses('apiKey')}
placeholder='Enter your API key'
required
/>
</div>
<div className='flex justify-end space-x-3 pt-4'>
{editingArr && (
<button
type='button'
onClick={handleDelete}
className='flex items-center px-3 py-2 text-sm rounded-lg
bg-red-600 hover:bg-red-700
text-white font-medium transition-colors'>
{deleteConfirm ? (
<>
<Check className='w-3.5 h-3.5 mr-2' />
Confirm Delete
</>
) : (
<>
<Trash className='w-3.5 h-3.5 mr-2' />
Delete
</>
)}
</button>
)}
<button
type='button'
onClick={handleTestConnection}
disabled={
isTestingConnection ||
!formData.arrServer ||
!formData.apiKey
}
className='flex items-center px-3 py-2 text-sm rounded-lg
bg-emerald-600 hover:bg-emerald-700
disabled:opacity-50 disabled:cursor-not-allowed
text-white font-medium transition-colors'>
{isTestingConnection ? (
<>
<Loader className='w-3.5 h-3.5 mr-2 animate-spin' />
Testing...
</>
) : testConfirm ? (
<>
<Check className='w-3.5 h-3.5 mr-2' />
Confirm Test
</>
) : (
<>
<TestTube className='w-3.5 h-3.5 mr-2' />
Test Connection
</>
)}
</button>
<button
type='submit'
className='flex items-center px-3 py-2 text-sm rounded-lg
bg-blue-600 hover:bg-blue-700
text-white font-medium transition-colors'>
{saveConfirm ? (
<>
<Check className='w-3.5 h-3.5 mr-2' />
Confirm {editingArr ? 'Update' : 'Add'}
</>
) : editingArr ? (
<>
<Save className='w-3.5 h-3.5 mr-2' />
Update
</>
) : (
<>
<Plus className='w-3.5 h-3.5 mr-2' />
Add
</>
)}
</button>
</div>
</form>
</Modal>
);
};
export default ArrModal;