/** * Metric Modal JavaScript * Handles modal open/close, tab switching, and metric data loading */ // Global state let currentMetricPath = null; let currentMetricData = null; /** * Open metric modal and load data * @param {string} metricPath - Path to metric YAML (e.g., 'finance/infra_cost.yml') or catalog FQN (e.g., 'catalog:...') */ function openMetricModal(metricPath) { currentMetricPath = metricPath; const overlay = document.getElementById('metricModalOverlay'); const body = document.getElementById('metricModalBody'); // Show modal overlay.classList.add('active'); document.body.style.overflow = 'hidden'; // Show loading state body.innerHTML = '
Loading metric...
'; // Route based on prefix: catalog:FQN uses /api/catalog/metrics, YAML paths use /api/metrics let url; if (metricPath.startsWith('catalog:')) { const fqn = metricPath.slice(8); // Remove 'catalog:' prefix url = `/api/catalog/metrics/${encodeURIComponent(fqn)}`; // URL-encode FQN } else { url = `/api/metrics/${metricPath}`; } // Fetch metric data fetch(url) .then(response => { if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } return response.json(); }) .then(data => { currentMetricData = data; renderMetricModal(data); }) .catch(error => { console.error('Error loading metric:', error); body.innerHTML = `
Failed to load metric: ${error.message}
`; }); } /** * Close metric modal */ function closeMetricModal() { const overlay = document.getElementById('metricModalOverlay'); overlay.classList.remove('active'); document.body.style.overflow = ''; currentMetricPath = null; currentMetricData = null; } /** * Switch between tabs * @param {string} tabId - Tab identifier */ function switchMetricTab(tabId) { // Update tab buttons document.querySelectorAll('.metric-tab').forEach(tab => { tab.classList.remove('active'); }); document.querySelector(`[data-tab="${tabId}"]`).classList.add('active'); // Update tab content document.querySelectorAll('.metric-tab-content').forEach(content => { content.classList.remove('active'); }); document.getElementById(tabId).classList.add('active'); } /** * Render metric modal content * @param {Object} data - Metric data from API */ function renderMetricModal(data) { const modal = document.getElementById('metricModal'); const titleElement = document.getElementById('metricModalTitle'); const metadataElement = document.getElementById('metricModalMetadata'); const body = document.getElementById('metricModalBody'); // Set category class for tab coloring const categoryClass = `category-${data.category}`; modal.setAttribute('data-category', data.category); // Set title and metadata (with technical name) titleElement.innerHTML = `
${data.display_name} ${data.validation && data.validation.status === 'validated' ? ` Validated ` : ''}
${data.name}
`; metadataElement.innerHTML = ` ${formatCategory(data.category)} ${data.metadata.grain ? `${data.metadata.grain}` : ''} ${data.metadata.unit ? `${data.metadata.unit}` : ''} `; // Apply category class to tabs document.querySelectorAll('.metric-tab').forEach(tab => { tab.className = tab.className.replace(/\bcategory-\w+/g, ''); tab.classList.add(categoryClass); }); // Render tab contents body.innerHTML = ` ${renderOverviewTab(data)} ${renderHowToUseTab(data)} ${renderSQLExamplesTab(data)} ${renderTechnicalTab(data)} `; // Activate first tab switchMetricTab('tabOverview'); // Apply syntax highlighting to all code blocks if (typeof Prism !== 'undefined') { Prism.highlightAll(); } } /** * Render Overview tab */ function renderOverviewTab(data) { const keyInsights = data.notes.key_insights || data.notes.all.slice(0, 5); // Prefer rendered HTML from catalog over stripped plain text const descriptionContent = data.overview.description_html ? `
${data.overview.description_html}
` : `

${escapeHtml(data.overview.description)}

`; return `

What it measures

${descriptionContent}
${keyInsights.length > 0 ? `

Key Insights

    ${keyInsights.map(note => `
  • ${highlightTechnicalTerms(escapeHtml(note))}
  • `).join('')}
` : ''} ${data.validation ? `

Validation

${data.validation.method}

${data.validation.result}

${data.validation.last_updated ? `

Last updated: ${data.validation.last_updated}

` : ''}
` : ''}
`; } /** * Render How to Use tab */ function renderHowToUseTab(data) { return `
${data.dimensions.length > 0 ? `

Dimensions

Filter and group this metric by:

${data.dimensions.map(dim => ` `).join('')}
` : ''} ${data.notes.all.length > 0 ? `

Important Notes

    ${data.notes.all.map(note => `
  • ${highlightTechnicalTerms(escapeHtml(note))}
  • `).join('')}
` : ''} ${data.special_sections && data.special_sections.cost_allocation_guide ? `

Cost Allocation Guide

${renderMarkdown(data.special_sections.cost_allocation_guide)}
` : ''}
`; } /** * Render SQL Examples tab */ function renderSQLExamplesTab(data) { const sqlExamples = data.sql_examples || {}; const simpleQueries = []; const advancedQueries = []; // Categorize queries by complexity Object.entries(sqlExamples).forEach(([key, example]) => { if (example.complexity === 'advanced') { advancedQueries.push([key, example]); } else { simpleQueries.push([key, example]); } }); return `
${simpleQueries.map(([key, example]) => renderCodeBlock(example.title, example.query, key)).join('')} ${advancedQueries.length > 0 ? `
${advancedQueries.map(([key, example]) => renderCodeBlock(example.title, example.query, key)).join('')}
` : ''}
`; } /** * Render Technical Details tab */ function renderTechnicalTab(data) { return `

Metric Configuration

Name ${data.name}
Type ${data.metadata.type}
Expression ${data.technical.expression}
Table ${data.technical.table}
Time Column ${data.metadata.time_column}
Grain ${data.metadata.grain}
${data.technical.data_sources && data.technical.data_sources.length > 0 ? `

Data Sources

Primary: ${data.technical.table}

${data.technical.data_sources.filter(ds => ds.type === 'join').length > 0 ? `

Joins:

    ${data.technical.data_sources.filter(ds => ds.type === 'join').map(ds => `
  • ${ds.table}${ds.via ? ` (via ${ds.via})` : ''}
  • ` ).join('')}
` : ''}
` : ''} ${data.technical.synonyms && data.technical.synonyms.length > 0 ? `

Synonyms

${data.technical.synonyms.map(syn => ` ${escapeHtml(syn)} `).join('')}
` : ''}
`; } /** * Render code block with copy button and syntax highlighting */ function renderCodeBlock(title, code, id) { return `
${title}
${escapeHtml(code)}
`; } /** * Copy code to clipboard */ function copyCode(elementId, button) { const code = document.getElementById(elementId).textContent; copyToClipboard(code, button); } /** * Copy dimension name to clipboard */ function copyDimension(button, text) { const originalText = button.textContent; navigator.clipboard.writeText(text).then(() => { button.textContent = '✓ Copied!'; button.style.background = '#D1FAE5'; button.style.color = '#065F46'; setTimeout(() => { button.textContent = originalText; button.style.background = ''; button.style.color = ''; }, 1500); }).catch(err => { console.error('Failed to copy:', err); }); } /** * Copy text to clipboard with visual feedback */ function copyToClipboard(text, button = null) { navigator.clipboard.writeText(text).then(() => { if (button) { const originalHTML = button.innerHTML; button.classList.add('copied'); button.innerHTML = ` Copied! `; setTimeout(() => { button.classList.remove('copied'); button.innerHTML = originalHTML; }, 2000); } }).catch(err => { console.error('Failed to copy:', err); }); } /** * Toggle expandable section */ function toggleExpandable(trigger) { const expandable = trigger.closest('.metric-expandable'); expandable.classList.toggle('expanded'); } /** * Format category name */ function formatCategory(category) { const map = { 'finance': 'Finance', 'product_usage': 'Product Usage', 'sales_revenue': 'Sales & Revenue', 'weekly_leadership_kpis': 'Weekly Leadership KPIs', 'revenue': 'Revenue', 'customers': 'Customers', 'marketing': 'Marketing', 'support': 'Support' }; return map[category] || category.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()); } /** * Simple markdown-to-HTML renderer (for cost_allocation_guide) */ function renderMarkdown(text) { return text .replace(/### (.*)/g, '

$1

') .replace(/## (.*)/g, '

$1

') .replace(/\*\*(.*?)\*\*/g, '$1') .replace(/\n\n/g, '

') .replace(/\n/g, '
'); } /** * Highlight technical terms in text (snake_case, table names, etc.) */ function highlightTechnicalTerms(text) { // Pattern: snake_case words, table names, technical identifiers // Match: gross_mrr, net_mrr, product_revenue, mrr_aggregated, etc. const pattern = /\b([a-z][a-z0-9]*_[a-z0-9_]+)\b/g; return text.replace(pattern, '$1'); } /** * Escape HTML to prevent XSS */ function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // Event Listeners document.addEventListener('DOMContentLoaded', () => { // Close modal on overlay click document.getElementById('metricModalOverlay')?.addEventListener('click', (e) => { if (e.target.id === 'metricModalOverlay') { closeMetricModal(); } }); // Close modal on Escape key document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && document.getElementById('metricModalOverlay')?.classList.contains('active')) { closeMetricModal(); } }); });