diff --git a/static/modals.js b/static/modals.js
index 7e68c6b..620ebb0 100644
--- a/static/modals.js
+++ b/static/modals.js
@@ -4,6 +4,80 @@
import { configStore, API_BASE_URL } from './global.js';
// =========================================================================
+// UTILITY HANDLERS (Copy & Download)
+// =========================================================================
+
+export function copyToClipboard(tabName) {
+ let contentToCopy = '';
+
+ // Get content safely
+ if (tabName === 'json') {
+ const jsonContentElement = document.getElementById('modal-json-content');
+ contentToCopy = jsonContentElement?.textContent?.trim() || '';
+ } else if (tabName === 'yaml') {
+ const yamlContentElement = document.getElementById('modal-yaml-content');
+ contentToCopy = yamlContentElement?.textContent?.trim() || '';
+ }
+
+ if (!contentToCopy) {
+ alert(`No ${tabName.toUpperCase()} content found to copy.`);
+ return;
+ }
+
+ // Clipboard API
+ navigator.clipboard.writeText(contentToCopy)
+ .then(() => {
+ const copyButton = document.getElementById('copy-config-button');
+ if (copyButton) {
+ // Instead of replacing innerHTML (which may destroy icons/styles),
+ // we temporarily change text via a data attribute or title
+ const originalLabel = copyButton.dataset.label || copyButton.textContent;
+ copyButton.dataset.label = originalLabel;
+
+ copyButton.classList.add('copied'); // optional styling via CSS
+ copyButton.textContent = '✅ Copied!';
+ setTimeout(() => {
+ copyButton.textContent = originalLabel;
+ copyButton.classList.remove('copied');
+ }, 1500);
+ } else {
+ alert('Configuration copied to clipboard!');
+ }
+ })
+ .catch(err => {
+ console.error('Failed to copy text: ', err);
+ alert('Failed to copy text. Please try again or check browser permissions.');
+ });
+}
+
+/**
+ * Downloads the YAML configuration content.
+ */
+export function downloadYaml() {
+ const yamlContent = document.getElementById('modal-yaml-content').textContent;
+ if (!yamlContent || yamlContent.trim() === '') {
+ alert("No YAML content available to download.");
+ return;
+ }
+
+ // Use modal title as filename fallback
+ const title = document.getElementById('modal-title').textContent
+ .replace(/\s+/g, '_')
+ .replace(/[^\w\-]/g, '');
+
+ const blob = new Blob([yamlContent], { type: 'text/yaml' });
+ const url = URL.createObjectURL(blob);
+
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = title ? `${title}.yaml` : 'config.yaml';
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+}
+
+// =========================================================================
// GENERIC MODAL HANDLERS
// =========================================================================
@@ -34,25 +108,36 @@
// CONFIGURATION DISPLAY MODAL HANDLERS (JSON/YAML tabs)
// =========================================================================
-/**
- * Switches between the JSON and YAML tabs in the main config modal.
- * This function MUST be exported as it's used directly in HTML/inline handlers.
- * @param {HTMLElement} modalContent - The parent container (modal-content) for tabs.
- * @param {string} tabName - 'json' or 'yaml'.
- */
export function switchTab(modalContent, tabName) {
- // Deactivate all buttons and hide all content
- modalContent.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active'));
- modalContent.querySelectorAll('.code-block').forEach(content => content.style.display = 'none');
+ // Deactivate all tab buttons
+ const tabSelection = modalContent.querySelector('.tab-selection');
+ if (tabSelection) {
+ tabSelection.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active'));
+ }
- // Activate the selected button and show the corresponding content
+ // Hide all code blocks
+ modalContent.querySelectorAll('.code-block').forEach(content => {
+ content.style.display = 'none';
+ });
+
+ // Activate selected tab button and content
const activeBtn = modalContent.querySelector(`.tab-button[data-tab="${tabName}"]`);
const activeContent = document.getElementById(`modal-${tabName}-content`);
-
if (activeBtn) activeBtn.classList.add('active');
if (activeContent) activeContent.style.display = 'block';
+
+ // Ensure copy button copies from the correct tab
+ const copyButton = document.getElementById('copy-config-button');
+ if (copyButton) {
+ // Remove any existing event listener safely
+ const newHandler = () => copyToClipboard(tabName);
+ copyButton.replaceWith(copyButton.cloneNode(true)); // clone to remove old listeners
+ const newCopyButton = document.getElementById('copy-config-button');
+ newCopyButton.addEventListener('click', newHandler);
+ }
}
+
/**
* Displays the main configuration modal (with JSON/YAML tabs). (RENAMED FROM showModal)
* @param {string} title - The modal title.
@@ -73,6 +158,7 @@
// Default to the specified tab
const modalContent = document.getElementById('configModal')?.querySelector('.modal-content');
if (modalContent) {
+ // This call to switchTab will also set the initial handler for the copy button
switchTab(modalContent, defaultTab);
}
@@ -97,11 +183,14 @@
modalContent.querySelectorAll('.tab-button').forEach(button => {
button.addEventListener('click', (event) => {
const tabName = event.target.getAttribute('data-tab');
- switchTab(modalContent, tabName);
+ // The switchTab function now includes the logic to update the copy button's target
+ switchTab(modalContent, tabName);
});
});
}
+// ... (Rest of the file remains the same) ...
+
// =========================================================================
// ADD FILTER CHAIN MODAL HANDLERS
// =========================================================================
@@ -237,30 +326,7 @@
// =========================================================================
// UTILITY HANDLERS (Download)
// =========================================================================
-
-export function downloadYaml() {
- const yamlContent = document.getElementById('modal-yaml-content').textContent;
- if (!yamlContent || yamlContent.trim() === '') {
- alert("No YAML content available to download.");
- return;
- }
-
- // Use modal title as filename fallback
- const title = document.getElementById('modal-title').textContent
- .replace(/\s+/g, '_')
- .replace(/[^\w\-]/g, '');
-
- const blob = new Blob([yamlContent], { type: 'text/yaml' });
- const url = URL.createObjectURL(blob);
-
- const a = document.createElement('a');
- a.href = url;
- a.download = title ? `${title}.yaml` : 'config.yaml';
- document.body.appendChild(a);
- a.click();
- document.body.removeChild(a);
- URL.revokeObjectURL(url);
-}
+// downloadYaml is now above the generic modal handlers section
/**
* Fetches and displays the full configuration for a listener in the tabbed modal.
@@ -304,7 +370,7 @@
* @param {string} defaultTab - The tab to show by default ('json' or 'yaml').
*/
export async function showSecretConfigModal(secretName) {
- const config = configStore.secrets[secretName];
+ const config = configStore.secrets[secretName];
if (!config) {
showConfigModal(`🚨 Error: Secret Not Found`, { name: secretName, error: 'Configuration data missing from memory.' }, 'Error: Secret not in memory.');
return;
@@ -356,4 +422,4 @@
}
showConfigModal(`Full Config for Cluster: ${clusterName}`, config, yamlData);
-}
+}
\ No newline at end of file
diff --git a/static/style.css b/static/style.css
index e782956..f02f027 100644
--- a/static/style.css
+++ b/static/style.css
@@ -487,49 +487,111 @@
margin-top: 0; /* Ensures consistent top margin on modal headers */
}
-/* Tab Controls Styling */
+/* --- TAB CONTROL LAYOUT --- */
.tab-controls {
+ display: flex; /* Aligns tab-selection and action-group horizontally */
+ align-items: flex-end; /* Aligns the tab buttons and action group to the bottom line */
+ margin: 0 0 0;
+ /* REMOVE: border-bottom: 2px solid var(--border-color); */
+}
+
+
+/* NEW CSS: Create a dedicated container for the gray underline */
+.tab-selection {
display: flex;
- align-items: flex-end;
- margin: 15px 0 0;
+ /* ADD the border-bottom here. It will only span the width of the tab buttons. */
border-bottom: 2px solid var(--border-color);
}
+/* Wrapper for Copy/Download buttons (right side) */
+.modal-action-group {
+ display: flex;
+ gap: 10px; /* Space between Copy and Download buttons */
+ align-items: center;
+ margin-left: auto; /* Pushes the group to the right */
+ padding-bottom: 5px; /* Visual adjustment above the border */
+}
+
+/* --- TAB BUTTON STYLING (JSON/YAML) --- */
.tab-button {
background: transparent;
border: none;
- border-bottom: 2px solid transparent;
- padding: 10px 15px;
+ padding: 10px 15px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
color: var(--secondary-color);
transition: all 0.2s ease;
- margin-bottom: -2px;
}
.tab-button.active {
color: var(--primary-color);
- border-bottom-color: var(--primary-color);
+ /* The active blue underline is still correct here */
+ border-bottom-color: var(--primary-color);
}
-/* Download Button Styling */
-.download-button {
- margin-left: auto;
+/* Style for the Copy Button to look like an action button */
+#copy-config-button {
+ /* Set to look like a secondary action button */
+ background-color: #fff;
+ color: var(--primary-color);
+ border: 1px solid var(--primary-color);
+ border-radius: 4px;
+ padding: 8px 12px;
+ font-size: 0.9rem;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.2s ease;
+
+ /* Override any tab-button specific styles that might clash */
+ margin-bottom: 0 !important;
+ line-height: 1.2;
+}
+
+#copy-config-button:hover {
background-color: var(--primary-color);
color: white;
+}
+
+/* Download Button Styling (Standardized to match the outlined look of Copy to Clip) */
+.download-button {
+ /* Set to match the Copy button: outlined style */
+ background-color: #fff; /* White background */
+ color: var(--primary-color); /* Primary text color */
+ border: 1px solid var(--primary-color); /* Primary border color */
border-radius: 4px;
- border-bottom: none !important;
padding: 8px 15px;
font-size: 0.9rem;
font-weight: 500;
}
.download-button:hover {
- background-color: #0b5ed7;
+ /* Solid primary color on hover for consistency */
+ background-color: var(--primary-color);
+ border-color: #0b5ed7;
+ color: white; /* White text on hover */
}
-/* Light Theme Code Block Styling (applies to both JSON and YAML) */
+/* Consistent Icon/Text Spacing for both action buttons */
+#copy-config-button,
+.download-button {
+ display: flex;
+ align-items: center;
+}
+
+/* Adjust spacing for the icons in both buttons */
+#copy-config-button .action-button-icon, /* Use a span class if you wrap the icon */
+.download-button .action-button-icon { /* Use a span class if you wrap the icon */
+ margin-right: 5px;
+}
+
+/* Since you are using unicode, this targets the first child (the emoji) */
+#copy-config-button:not([style*='Copied']) > *:first-child,
+.download-button > *:first-child {
+ margin-right: 5px; /* Small space between icon and text */
+}
+
+/* Code Block Styling */
.code-block {
background: var(--code-bg);
color: var(--code-text);
@@ -757,9 +819,6 @@
text-decoration: none; /* Remove underline from the anchor tag */
transition: all 0.2s ease;
- /* Ensure height is consistent with existing toolbar buttons */
- height: 42; /* Based on 8px padding + 0.9rem font size in .toolbar button */
-
/* Add a subtle shadow consistent with primary toolbar buttons */
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}