Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
248 changes: 248 additions & 0 deletions mcpgateway/static/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -5103,8 +5103,49 @@ async function editServer(serverId) {
);
}

// Set associated resources data attribute on the container
const editResourcesContainer = document.getElementById(
"edit-server-resources",
);
if (editResourcesContainer && server.associatedResources) {
editResourcesContainer.setAttribute(
"data-server-resources",
JSON.stringify(server.associatedResources),
);
}

// Set associated prompts data attribute on the container
const editPromptsContainer = document.getElementById(
"edit-server-prompts",
);
if (editPromptsContainer && server.associatedPrompts) {
editPromptsContainer.setAttribute(
"data-server-prompts",
JSON.stringify(server.associatedPrompts),
);
}

openModal("server-edit-modal");

// Initialize the select handlers for resources and prompts in the edit modal
initResourceSelect(
"edit-server-resources",
"selectedEditResourcesPills",
"selectedEditResourcesWarning",
6,
"selectAllEditResourcesBtn",
"clearAllEditResourcesBtn",
);

initPromptSelect(
"edit-server-prompts",
"selectedEditPromptsPills",
"selectedEditPromptsWarning",
6,
"selectAllEditPromptsBtn",
"clearAllEditPromptsBtn",
);

// Use multiple approaches to ensure checkboxes get set
setEditServerAssociations(server);
setTimeout(() => setEditServerAssociations(server), 100);
Expand Down Expand Up @@ -5452,6 +5493,213 @@ if (window.htmx && !window._toolsHtmxHandlerAttached) {
});
}

// Set up HTMX handler for auto-checking newly loaded resources when Select All is active
if (window.htmx && !window._resourcesHtmxHandlerAttached) {
window._resourcesHtmxHandlerAttached = true;

window.htmx.on("htmx:afterSettle", function (evt) {
// Only handle resource pagination requests
if (
evt.detail.pathInfo &&
evt.detail.pathInfo.requestPath &&
evt.detail.pathInfo.requestPath.includes("/admin/resources/partial")
) {
setTimeout(() => {
// Find the container
let container = null;
const target = evt.detail.target;

if (target && target.id === "edit-server-resources") {
container = target;
} else if (target && target.id === "associatedResources") {
container = target;
} else if (target) {
container =
target.closest("#associatedResources") ||
target.closest("#edit-server-resources");
}

if (!container) {
const editModal =
document.getElementById("server-edit-modal");
const isEditModalOpen =
editModal && !editModal.classList.contains("hidden");

if (isEditModalOpen) {
container = document.getElementById(
"edit-server-resources",
);
} else {
container = document.getElementById(
"associatedResources",
);
}
}

if (container) {
const newCheckboxes = container.querySelectorAll(
"input[data-auto-check=true]",
);

const selectAllInput = container.querySelector(
'input[name="selectAllResources"]',
);

// Check if Select All is active
if (selectAllInput && selectAllInput.value === "true") {
newCheckboxes.forEach((cb) => {
cb.checked = true;
cb.removeAttribute("data-auto-check");
});

if (newCheckboxes.length > 0) {
const event = new Event("change", {
bubbles: true,
});
container.dispatchEvent(event);
}
}

// Also check for edit mode: pre-select items based on server's associated resources
const dataAttr = container.getAttribute(
"data-server-resources",
);
if (dataAttr) {
try {
const associatedResourceIds = JSON.parse(dataAttr);
newCheckboxes.forEach((cb) => {
const checkboxValue = parseInt(cb.value);
if (
associatedResourceIds.includes(
checkboxValue,
)
) {
cb.checked = true;
}
cb.removeAttribute("data-auto-check");
});

if (newCheckboxes.length > 0) {
const event = new Event("change", {
bubbles: true,
});
container.dispatchEvent(event);
}
} catch (e) {
console.error(
"Error parsing data-server-resources:",
e,
);
}
}
}
}, 10);
}
});
}

// Set up HTMX handler for auto-checking newly loaded prompts when Select All is active
if (window.htmx && !window._promptsHtmxHandlerAttached) {
window._promptsHtmxHandlerAttached = true;

window.htmx.on("htmx:afterSettle", function (evt) {
// Only handle prompt pagination requests
if (
evt.detail.pathInfo &&
evt.detail.pathInfo.requestPath &&
evt.detail.pathInfo.requestPath.includes("/admin/prompts/partial")
) {
setTimeout(() => {
// Find the container
let container = null;
const target = evt.detail.target;

if (target && target.id === "edit-server-prompts") {
container = target;
} else if (target && target.id === "associatedPrompts") {
container = target;
} else if (target) {
container =
target.closest("#associatedPrompts") ||
target.closest("#edit-server-prompts");
}

if (!container) {
const editModal =
document.getElementById("server-edit-modal");
const isEditModalOpen =
editModal && !editModal.classList.contains("hidden");

if (isEditModalOpen) {
container = document.getElementById(
"edit-server-prompts",
);
} else {
container =
document.getElementById("associatedPrompts");
}
}

if (container) {
const newCheckboxes = container.querySelectorAll(
"input[data-auto-check=true]",
);

const selectAllInput = container.querySelector(
'input[name="selectAllPrompts"]',
);

// Check if Select All is active
if (selectAllInput && selectAllInput.value === "true") {
newCheckboxes.forEach((cb) => {
cb.checked = true;
cb.removeAttribute("data-auto-check");
});

if (newCheckboxes.length > 0) {
const event = new Event("change", {
bubbles: true,
});
container.dispatchEvent(event);
}
}

// Also check for edit mode: pre-select items based on server's associated prompts
const dataAttr = container.getAttribute(
"data-server-prompts",
);
if (dataAttr) {
try {
const associatedPromptIds = JSON.parse(dataAttr);
newCheckboxes.forEach((cb) => {
const checkboxValue = parseInt(cb.value);
if (
associatedPromptIds.includes(checkboxValue)
) {
cb.checked = true;
}
cb.removeAttribute("data-auto-check");
});

if (newCheckboxes.length > 0) {
const event = new Event("change", {
bubbles: true,
});
container.dispatchEvent(event);
}
} catch (e) {
console.error(
"Error parsing data-server-prompts:",
e,
);
}
}
}
}, 10);
}
});
}

// ===================================================================
// ENHANCED TAB HANDLING with Better Error Management
// ===================================================================
Expand Down
112 changes: 29 additions & 83 deletions mcpgateway/templates/admin.html
Original file line number Diff line number Diff line change
Expand Up @@ -8876,20 +8876,19 @@ <h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">
<div
id="edit-server-resources"
class="max-h-60 overflow-y-auto rounded-md border border-gray-600 shadow-sm p-3 bg-gray-50 dark:bg-gray-900"
hx-get="{{ root_path }}/admin/resources/partial?page=1&per_page=50&render=selector"
hx-trigger="load"
hx-swap="innerHTML"
hx-on::after-swap="initResourceSelect('edit-server-resources', 'selectedEditResourcesPills', 'selectedEditResourcesWarning', 6, 'selectAllEditResourcesBtn', 'clearAllEditResourcesBtn')"
>
{% for resource in resources %}
<label
class="flex items-center space-x-3 text-gray-700 dark:text-gray-300 mb-2 cursor-pointer hover:bg-blue-50 dark:hover:bg-blue-900 rounded-md p-1"
>
<input
type="checkbox"
name="associatedResources"
value="{{ resource.id }}"
class="resource-checkbox form-checkbox h-5 w-5 text-blue-600 dark:bg-gray-800 dark:border-gray-600"
/>
<span class="select-none">{{ resource.name }}</span>
</label>
{% endfor %}
<!-- Resources will be loaded via HTMX -->
<div class="text-center py-4 text-gray-500 dark:text-gray-400">
<svg class="animate-spin h-5 w-5 text-indigo-600 dark:text-indigo-400 inline-block" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span class="ml-2">Loading resources...</span>
</div>
</div>
<div class="flex justify-end gap-3 mt-3">
<button
Expand Down Expand Up @@ -8931,20 +8930,19 @@ <h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">
<div
id="edit-server-prompts"
class="max-h-60 overflow-y-auto rounded-md border border-gray-600 shadow-sm p-3 bg-gray-50 dark:bg-gray-900"
hx-get="{{ root_path }}/admin/prompts/partial?page=1&per_page=50&render=selector"
hx-trigger="load"
hx-swap="innerHTML"
hx-on::after-swap="initPromptSelect('edit-server-prompts', 'selectedEditPromptsPills', 'selectedEditPromptsWarning', 6, 'selectAllEditPromptsBtn', 'clearAllEditPromptsBtn')"
>
{% for prompt in prompts %}
<label
class="flex items-center space-x-3 text-gray-700 dark:text-gray-300 mb-2 cursor-pointer hover:bg-purple-50 dark:hover:bg-purple-900 rounded-md p-1"
>
<input
type="checkbox"
name="associatedPrompts"
value="{{ prompt.id }}"
class="prompt-checkbox form-checkbox h-5 w-5 text-purple-600 dark:bg-gray-800 dark:border-gray-600"
/>
<span class="select-none">{{ prompt.name }}</span>
</label>
{% endfor %}
<!-- Prompts will be loaded via HTMX -->
<div class="text-center py-4 text-gray-500 dark:text-gray-400">
<svg class="animate-spin h-5 w-5 text-indigo-600 dark:text-indigo-400 inline-block" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span class="ml-2">Loading prompts...</span>
</div>
</div>
<div class="flex justify-end gap-3 mt-3">
<button
Expand Down Expand Up @@ -12639,63 +12637,11 @@ <h3 class="text-lg font-medium text-gray-900 dark:text-white mb-4">
}
});

// Select All / Clear All handlers for create server form
document.addEventListener('DOMContentLoaded', function() {
// Resources Select All / Clear All for create form
const selectAllResourcesBtn = document.getElementById('selectAllResourcesBtn');
const clearAllResourcesBtn = document.getElementById('clearAllResourcesBtn');
if (selectAllResourcesBtn && clearAllResourcesBtn) {
selectAllResourcesBtn.addEventListener('click', function() {
const checkboxes = document.querySelectorAll('#associatedResources input[type="checkbox"][name="associatedResources"]');
checkboxes.forEach(checkbox => checkbox.checked = true);
});
clearAllResourcesBtn.addEventListener('click', function() {
const checkboxes = document.querySelectorAll('#associatedResources input[type="checkbox"][name="associatedResources"]');
checkboxes.forEach(checkbox => checkbox.checked = false);
});
}

// Prompts Select All / Clear All for create form
const selectAllPromptsBtn = document.getElementById('selectAllPromptsBtn');
const clearAllPromptsBtn = document.getElementById('clearAllPromptsBtn');
if (selectAllPromptsBtn && clearAllPromptsBtn) {
selectAllPromptsBtn.addEventListener('click', function() {
const checkboxes = document.querySelectorAll('#associatedPrompts input[type="checkbox"][name="associatedPrompts"]');
checkboxes.forEach(checkbox => checkbox.checked = true);
});
clearAllPromptsBtn.addEventListener('click', function() {
const checkboxes = document.querySelectorAll('#associatedPrompts input[type="checkbox"][name="associatedPrompts"]');
checkboxes.forEach(checkbox => checkbox.checked = false);
});
}

// Select All / Clear All handlers for edit server form (should already exist but let's add them)
const selectAllEditResourcesBtn = document.getElementById('selectAllEditResourcesBtn');
const clearAllEditResourcesBtn = document.getElementById('clearAllEditResourcesBtn');
if (selectAllEditResourcesBtn && clearAllEditResourcesBtn) {
selectAllEditResourcesBtn.addEventListener('click', function() {
const checkboxes = document.querySelectorAll('#edit-server-resources input[type="checkbox"][name="associatedResources"]');
checkboxes.forEach(checkbox => checkbox.checked = true);
});
clearAllEditResourcesBtn.addEventListener('click', function() {
const checkboxes = document.querySelectorAll('#edit-server-resources input[type="checkbox"][name="associatedResources"]');
checkboxes.forEach(checkbox => checkbox.checked = false);
});
}

const selectAllEditPromptsBtn = document.getElementById('selectAllEditPromptsBtn');
const clearAllEditPromptsBtn = document.getElementById('clearAllEditPromptsBtn');
if (selectAllEditPromptsBtn && clearAllEditPromptsBtn) {
selectAllEditPromptsBtn.addEventListener('click', function() {
const checkboxes = document.querySelectorAll('#edit-server-prompts input[type="checkbox"][name="associatedPrompts"]');
checkboxes.forEach(checkbox => checkbox.checked = true);
});
clearAllEditPromptsBtn.addEventListener('click', function() {
const checkboxes = document.querySelectorAll('#edit-server-prompts input[type="checkbox"][name="associatedPrompts"]');
checkboxes.forEach(checkbox => checkbox.checked = false);
});
}
});
// NOTE: Select All / Clear All handlers for Resources and Prompts are now handled
// by initResourceSelect() and initPromptSelect() functions in admin.js.
// Those functions properly fetch ALL IDs from the server and maintain state,
// so items that load via pagination are also included in the selection.
// The handlers are attached via HTMX's hx-on::after-swap attribute.

// Download sample JSON for bulk import
function downloadSampleJSON() {
Expand Down
Loading