/**
* 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')
*/
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 = '
';
// Fetch metric data
fetch(`/api/metrics/${metricPath}`)
.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);
return `
${data.overview.description}
${keyInsights.length > 0 ? `
${keyInsights.map(note => `${highlightTechnicalTerms(escapeHtml(note))} `).join('')}
` : ''}
${data.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 ? `
Filter and group this metric by:
${data.dimensions.map(dim => `
${dim}
`).join('')}
` : ''}
${data.notes.all.length > 0 ? `
${data.notes.all.map(note => `${highlightTechnicalTerms(escapeHtml(note))} `).join('')}
` : ''}
${data.special_sections && data.special_sections.cost_allocation_guide ? `
How to allocate infrastructure cost to customers
${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 ? `
Show advanced queries
${advancedQueries.map(([key, example]) => renderCodeBlock(example.title, example.query, key)).join('')}
` : ''}
`;
}
/**
* Render Technical Details tab
*/
function renderTechnicalTab(data) {
return `
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 ? `
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 ? `
${data.technical.synonyms.map(syn => `
${escapeHtml(syn)}
`).join('')}
` : ''}
`;
}
/**
* Render code block with copy button and syntax highlighting
*/
function renderCodeBlock(title, code, id) {
return `
`;
}
/**
* 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();
}
});
});