Skip to content

Commit 13d25ef

Browse files
committed
feat: enhance instance management by adding dynamic add/remove functionality and improving UI elements
1 parent d666ba7 commit 13d25ef

File tree

3 files changed

+226
-14
lines changed

3 files changed

+226
-14
lines changed

frontend/static/css/style.css

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1519,13 +1519,15 @@ label .info-icon ~ text {
15191519
background-color: #dc3545;
15201520
color: white;
15211521
border: none;
1522-
width: 30px;
1523-
height: 30px;
1522+
padding: 6px 12px;
15241523
border-radius: 4px;
15251524
cursor: pointer;
1526-
display: flex;
1525+
display: inline-flex;
15271526
align-items: center;
1528-
justify-content: center;
1527+
gap: 5px;
1528+
font-size: 13px;
1529+
margin-top: 5px;
1530+
transition: background-color 0.2s;
15291531
}
15301532

15311533
.remove-instance-btn:hover {

frontend/static/js/new-main.js

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1289,17 +1289,18 @@ let huntarrUI = {
12891289
console.log(`[huntarrUI] Found ${instanceItems.length} instance items for ${app}. Processing multi-instance mode.`);
12901290
// Multi-instance logic (current Sonarr logic)
12911291
instanceItems.forEach((item, index) => {
1292-
const instanceId = item.dataset.instanceId; // Assumes Sonarr uses data-instance-id
1293-
const nameInput = form.querySelector(`#${app}_instance_${instanceId}_name`);
1294-
const urlInput = form.querySelector(`#${app}_instance_${instanceId}_api_url`);
1295-
const keyInput = form.querySelector(`#${app}_instance_${instanceId}_api_key`);
1296-
const enabledInput = item.querySelector('.instance-enabled'); // Assumes Sonarr uses this class for enable toggle
1292+
const instanceId = item.dataset.instanceId; // Gets the data-instance-id
1293+
const nameInput = form.querySelector(`#${app}-name-${instanceId}`);
1294+
const urlInput = form.querySelector(`#${app}-url-${instanceId}`);
1295+
const keyInput = form.querySelector(`#${app}-key-${instanceId}`);
1296+
const enabledInput = form.querySelector(`#${app}-enabled-${instanceId}`);
12971297

12981298
if (urlInput && keyInput) { // Need URL and Key at least
12991299
settings.instances.push({
13001300
// Use nameInput value if available, otherwise generate a default
13011301
name: nameInput && nameInput.value.trim() !== '' ? nameInput.value.trim() : `Instance ${index + 1}`,
13021302
api_url: this.cleanUrlString(urlInput.value),
1303+
api_key: keyInput.value.trim(),
13031304
// Default to true if toggle doesn't exist or is checked
13041305
enabled: enabledInput ? enabledInput.checked : true
13051306
});
@@ -1339,11 +1340,10 @@ let huntarrUI = {
13391340
instanceItems.forEach((item) => {
13401341
const instanceId = item.dataset.instanceId;
13411342
if(instanceId) {
1342-
handledInstanceFieldIds.add(`${app}_instance_${instanceId}_name`);
1343-
handledInstanceFieldIds.add(`${app}_instance_${instanceId}_api_url`);
1344-
handledInstanceFieldIds.add(`${app}_instance_${instanceId}_api_key`);
1345-
const enabledToggle = item.querySelector('.instance-enabled');
1346-
if (enabledToggle && enabledToggle.id) handledInstanceFieldIds.add(enabledToggle.id);
1343+
handledInstanceFieldIds.add(`${app}-name-${instanceId}`);
1344+
handledInstanceFieldIds.add(`${app}-url-${instanceId}`);
1345+
handledInstanceFieldIds.add(`${app}-key-${instanceId}`);
1346+
handledInstanceFieldIds.add(`${app}-enabled-${instanceId}`);
13471347
}
13481348
});
13491349
} else {

frontend/static/js/settings_forms.js

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1620,6 +1620,216 @@ const SettingsForms = {
16201620
});
16211621
});
16221622
});
1623+
1624+
// Set up remove buttons for existing instances
1625+
const removeButtons = container.querySelectorAll('.remove-instance-btn');
1626+
removeButtons.forEach(btn => {
1627+
btn.addEventListener('click', function() {
1628+
const instancePanel = btn.closest('.instance-item') || btn.closest('.instance-panel');
1629+
if (instancePanel && instancePanel.parentNode) {
1630+
instancePanel.parentNode.removeChild(instancePanel);
1631+
1632+
// Update the button text with new count if updateAddButtonText exists
1633+
const addBtn = container.querySelector(`.add-${appType}-instance-btn`);
1634+
if (addBtn) {
1635+
const instancesContainer = container.querySelector('.instances-container');
1636+
if (instancesContainer) {
1637+
const currentCount = instancesContainer.querySelectorAll('.instance-item').length;
1638+
addBtn.innerHTML = `<i class="fas fa-plus"></i> Add ${appType.charAt(0).toUpperCase() + appType.slice(1)} Instance (${currentCount}/9)`;
1639+
1640+
// Re-enable button if we're under the limit
1641+
if (currentCount < 9) {
1642+
addBtn.disabled = false;
1643+
addBtn.title = "";
1644+
}
1645+
}
1646+
}
1647+
1648+
// Trigger change event to update save button state
1649+
const changeEvent = new Event('change');
1650+
container.dispatchEvent(changeEvent);
1651+
}
1652+
});
1653+
});
1654+
1655+
// Add instance button functionality
1656+
const addBtn = container.querySelector(`.add-${appType}-instance-btn`);
1657+
if (addBtn) {
1658+
// Function to update the button text with current instance count
1659+
const updateAddButtonText = () => {
1660+
const instancesContainer = container.querySelector('.instances-container');
1661+
if (!instancesContainer) return;
1662+
const currentCount = instancesContainer.querySelectorAll('.instance-item').length;
1663+
addBtn.innerHTML = `<i class="fas fa-plus"></i> Add ${appType.charAt(0).toUpperCase() + appType.slice(1)} Instance (${currentCount}/9)`;
1664+
1665+
// Disable button if we've reached the limit
1666+
if (currentCount >= 9) {
1667+
addBtn.disabled = true;
1668+
addBtn.title = "Maximum of 9 instances allowed";
1669+
} else {
1670+
addBtn.disabled = false;
1671+
addBtn.title = "";
1672+
}
1673+
};
1674+
1675+
// Initial button text update
1676+
updateAddButtonText();
1677+
1678+
// Add event listener for the add button
1679+
addBtn.addEventListener('click', function() {
1680+
const instancesContainer = container.querySelector('.instances-container');
1681+
if (!instancesContainer) return;
1682+
1683+
const existingInstances = instancesContainer.querySelectorAll('.instance-item');
1684+
const currentCount = existingInstances.length;
1685+
1686+
// Don't allow more than 9 instances
1687+
if (currentCount >= 9) {
1688+
alert('Maximum of 9 instances allowed');
1689+
return;
1690+
}
1691+
1692+
const newIndex = currentCount; // Use current count as new index
1693+
1694+
// Create new instance HTML
1695+
const newInstanceHtml = `
1696+
<div class="instance-item" data-instance-id="${newIndex}">
1697+
<div class="instance-header">
1698+
<h4>Instance ${newIndex + 1}: New Instance</h4>
1699+
<div class="instance-actions">
1700+
<button type="button" class="remove-instance-btn">Remove</button>
1701+
<button type="button" class="test-connection-btn" data-instance="${newIndex}" style="margin-left: 10px;">
1702+
<i class="fas fa-plug"></i> Test Connection
1703+
</button>
1704+
</div>
1705+
</div>
1706+
<div class="instance-content">
1707+
<div class="setting-item">
1708+
<label for="${appType}-name-${newIndex}">Name:</label>
1709+
<input type="text" id="${appType}-name-${newIndex}" name="name" value="" placeholder="Friendly name for this ${appType} instance">
1710+
<p class="setting-help">Friendly name for this ${appType} instance</p>
1711+
</div>
1712+
<div class="setting-item">
1713+
<label for="${appType}-url-${newIndex}">URL:</label>
1714+
<input type="text" id="${appType}-url-${newIndex}" name="api_url" value="" placeholder="Base URL for ${appType} (e.g., http://localhost:8989)">
1715+
<p class="setting-help">Base URL for ${appType}</p>
1716+
</div>
1717+
<div class="setting-item">
1718+
<label for="${appType}-key-${newIndex}">API Key:</label>
1719+
<input type="text" id="${appType}-key-${newIndex}" name="api_key" value="" placeholder="API key for ${appType}">
1720+
<p class="setting-help">API key for ${appType}</p>
1721+
</div>
1722+
<div class="setting-item">
1723+
<label for="${appType}-enabled-${newIndex}">Enabled:</label>
1724+
<label class="toggle-switch" style="width:40px; height:20px; display:inline-block; position:relative;">
1725+
<input type="checkbox" id="${appType}-enabled-${newIndex}" name="enabled" checked>
1726+
<span class="toggle-slider" style="position:absolute; cursor:pointer; top:0; left:0; right:0; bottom:0; background-color:#3d4353; border-radius:20px; transition:0.4s;"></span>
1727+
</label>
1728+
</div>
1729+
</div>
1730+
</div>
1731+
`;
1732+
1733+
// Add the new instance to the container
1734+
instancesContainer.insertAdjacentHTML('beforeend', newInstanceHtml);
1735+
1736+
// Get the newly added instance element
1737+
const newInstance = instancesContainer.querySelector(`[data-instance-id="${newIndex}"]`);
1738+
1739+
// Set up event listeners for the new instance's buttons
1740+
const newTestBtn = newInstance.querySelector('.test-connection-btn');
1741+
const newRemoveBtn = newInstance.querySelector('.remove-instance-btn');
1742+
1743+
// Test connection button
1744+
if (newTestBtn) {
1745+
newTestBtn.addEventListener('click', (e) => {
1746+
e.preventDefault();
1747+
const instancePanel = newTestBtn.closest('.instance-item');
1748+
const urlInput = instancePanel.querySelector('input[name="api_url"]');
1749+
const keyInput = instancePanel.querySelector('input[name="api_key"]');
1750+
1751+
if (!urlInput || !keyInput) {
1752+
alert('Error: Could not find URL or API key inputs');
1753+
return;
1754+
}
1755+
1756+
const url = urlInput.value.trim();
1757+
const apiKey = keyInput.value.trim();
1758+
1759+
if (!url) {
1760+
alert('Please enter a valid URL');
1761+
urlInput.focus();
1762+
return;
1763+
}
1764+
1765+
if (!apiKey) {
1766+
alert('Please enter a valid API key');
1767+
keyInput.focus();
1768+
return;
1769+
}
1770+
1771+
// Use the same test connection logic as existing buttons
1772+
const originalButtonHTML = newTestBtn.innerHTML;
1773+
newTestBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Testing...';
1774+
newTestBtn.disabled = true;
1775+
1776+
HuntarrUtils.fetchWithTimeout(`/api/${appType}/test-connection`, {
1777+
method: 'POST',
1778+
headers: { 'Content-Type': 'application/json' },
1779+
body: JSON.stringify({ api_url: url, api_key: apiKey })
1780+
})
1781+
.then(response => response.json())
1782+
.then(data => {
1783+
newTestBtn.disabled = false;
1784+
if (data.success) {
1785+
newTestBtn.innerHTML = '<i class="fas fa-check"></i> Connected!';
1786+
alert(`Successfully connected to ${appType}`);
1787+
setTimeout(() => {
1788+
newTestBtn.innerHTML = originalButtonHTML;
1789+
}, 3000);
1790+
} else {
1791+
newTestBtn.innerHTML = '<i class="fas fa-times"></i> Failed';
1792+
alert(`Connection failed: ${data.message || 'Unknown error'}`);
1793+
setTimeout(() => {
1794+
newTestBtn.innerHTML = originalButtonHTML;
1795+
}, 3000);
1796+
}
1797+
})
1798+
.catch(error => {
1799+
newTestBtn.disabled = false;
1800+
newTestBtn.innerHTML = '<i class="fas fa-times"></i> Error';
1801+
alert(`Connection test failed: ${error.message}`);
1802+
setTimeout(() => {
1803+
newTestBtn.innerHTML = originalButtonHTML;
1804+
}, 3000);
1805+
});
1806+
});
1807+
}
1808+
1809+
// Remove button
1810+
if (newRemoveBtn) {
1811+
newRemoveBtn.addEventListener('click', function() {
1812+
newInstance.remove();
1813+
updateAddButtonText();
1814+
1815+
// Trigger change event
1816+
const changeEvent = new Event('change');
1817+
container.dispatchEvent(changeEvent);
1818+
});
1819+
}
1820+
1821+
// Update button text and trigger change event
1822+
updateAddButtonText();
1823+
const changeEvent = new Event('change');
1824+
container.dispatchEvent(changeEvent);
1825+
1826+
// Focus on the name input of the new instance
1827+
const nameInput = newInstance.querySelector('input[name="name"]');
1828+
if (nameInput) {
1829+
nameInput.focus();
1830+
}
1831+
});
1832+
}
16231833
},
16241834

16251835
// Test connection to an *arr API

0 commit comments

Comments
 (0)