// ============================================================================= // COMPLETE DBT-DOCS METRICS INTEGRATION // ============================================================================= // This file contains all the code changes needed to integrate YAML-based // metrics from model configurations into dbt-docs natively. // // Implementation Order: // 1. Service Layer (project_service.js) // 2. Routing (app.js) // 3. Metric Controller & Template // 4. Model Enhancement // 5. Navigation Updates // =============================================================================
// ============================================================================= // 1. SERVICE LAYER INTEGRATION // File: src/app/services/project_service.js // Add after existing model processing logic (around line 200) // =============================================================================
// Extract and process metrics from model meta function processModelMetrics(nodes) { var extractedMetrics = {};
_.each(nodes, function(node) {
if (node.resource_type === 'model') {
var modelMeta = node.meta || {};
var modelMetrics = modelMeta.metrics || {};
if (Object.keys(modelMetrics).length > 0) {
_.each(modelMetrics, function(metricConfig, metricName) {
var metricNode = createMetricNode(node, metricName, metricConfig);
extractedMetrics[metricNode.unique_id] = metricNode;
// Also add to main nodes collection
nodes[metricNode.unique_id] = metricNode;
});
// Add metrics reference to the model
node.associated_metrics = Object.keys(modelMetrics).map(function(metricName) {
return 'metric.' + node.package_name + '.' + node.name + '.' + metricName;
});
}
}
});
return extractedMetrics;
}
function createMetricNode(modelNode, metricName, metricConfig) { // Include model name in unique_id to prevent collisions var uniqueId = 'metric.' + modelNode.package_name + '.' + modelNode.name + '.' + metricName;
return {
unique_id: uniqueId,
name: metricName,
label: metricConfig.label || metricName,
resource_type: 'metric',
package_name: modelNode.package_name,
description: metricConfig.description || '',
// Keep model context
model: modelNode.name,
model_unique_id: modelNode.unique_id,
// Add a display name that shows context
display_name: modelNode.name + '.' + metricName,
// All other properties remain the same...
type: metricConfig.type || 'calculated',
method: metricConfig.method || 'sql',
column: metricConfig.column || '',
sql: metricConfig.sql || '',
macro: metricConfig.macro || '',
filters: metricConfig.filters || [],
groups: metricConfig.groups || [],
format: metricConfig.format || '',
parameters: metricConfig.parameters || {},
meta: metricConfig,
tags: metricConfig.tags || [],
depends_on: {
nodes: [modelNode.unique_id],
macros: metricConfig.macro ? ['macro.' + modelNode.package_name + '.' + metricConfig.macro] : []
},
original_file_path: modelNode.original_file_path,
docs: {
show: true
}
};
}
// Add to buildMetricTree function in project_service.js (around line 800) // Enhanced buildMetricTree function - Type-First with Model grouping function buildMetricTree(nodes, select) { var metrics = {};
_.each(nodes, function(node) {
if (node.resource_type !== 'metric') return;
var metricName = node.label || node.name;
var metricType = node.type || 'calculated';
var modelName = node.model || 'unknown';
var is_active = node.unique_id == select;
// Create type group if it doesn't exist
if (!metrics[metricType]) {
metrics[metricType] = {
type: "folder",
name: metricType.charAt(0).toUpperCase() + metricType.slice(1) + ' Metrics',
active: false,
items: {},
metricType: metricType
};
}
// Create model subgroup within type if it doesn't exist
if (!metrics[metricType].items[modelName]) {
metrics[metricType].items[modelName] = {
type: "folder",
name: modelName,
active: false,
items: [],
modelName: modelName,
metricType: metricType
};
}
// Update active state bubbling up
if (is_active) {
metrics[metricType].active = true;
metrics[metricType].items[modelName].active = true;
}
// Add the metric to the model subgroup
metrics[metricType].items[modelName].items.push({
type: 'file',
name: metricName,
node: node,
active: is_active,
unique_id: node.unique_id,
node_type: 'metric',
model: modelName,
metricType: metricType
});
});
// Convert to array structure and sort
var metrics_array = _.map(metrics, function(typeGroup, typeName) {
// Convert model items to array and sort
typeGroup.items = _.sortBy(_.values(typeGroup.items), 'name');
// Sort metrics within each model
_.each(typeGroup.items, function(modelGroup) {
modelGroup.items = _.sortBy(modelGroup.items, 'name');
});
return typeGroup;
});
// Sort by metric type
return _.sortBy(metrics_array, 'name');
}
// Add these calls after the existing node processing (around line 250) // Process metrics from model meta var extractedMetrics = processModelMetrics(service.files.manifest.nodes); console.log('π Extracted', Object.keys(extractedMetrics).length, 'metrics from model meta');
// Update getModelTree function to include metrics // Add this call in getModelTree function (around line 850) var metrics_from_meta = .filter(.values(service.project.nodes), function(node) { return node.resource_type === 'metric'; }); service.tree.metrics = buildMetricTree(metrics_from_meta, select);
// Update searchable collection to include metrics // Modify search_nodes filter (around line 600) var search_nodes = _.filter(service.project.nodes, function(node) { return _.includes(['model', 'source', 'seed', 'snapshot', 'analysis', 'exposure', 'metric'], node.resource_type); });
// ============================================================================= // 2. ROUTING CONFIGURATION // File: src/app/app.js // Add to state definitions // =============================================================================
.state('dbt.metric', { url: "/metric/:unique_id?g_v&g_i&g_e", templateUrl: require('./docs/metric.html'), controller: 'MetricCtrl' })
// ============================================================================= // 3. METRIC CONTROLLER // File: src/app/docs/metric.js (NEW FILE) // =============================================================================
'use strict';
var angular = require('angular'); var dag_utils = require('./dag_utils');
angular .module('dbt') .controller('MetricCtrl', ['$scope', '$state', 'project', 'code', function($scope, $state, projectService, codeService) {
$scope.model_uid = $state.params.unique_id;
$scope.project = projectService;
$scope.codeService = codeService;
$scope.extra_table_fields = [];
$scope.usage_examples = [];
$scope.metric = {};
projectService.ready(function(project) {
var metric = project.nodes[$scope.model_uid];
$scope.metric = metric || {};
if (!metric) {
console.error('Metric not found:', $scope.model_uid);
return;
}
$scope.parents = dag_utils.getParents(project, metric);
$scope.parentsLength = Object.keys($scope.parents).length;
// Build extra fields for the details table
$scope.extra_table_fields = [
{ name: "Type", value: $scope.metric.type },
{ name: "Method", value: $scope.metric.method },
{ name: "Source Model", value: $scope.metric.model }
];
// Add optional fields
if ($scope.metric.column) {
$scope.extra_table_fields.push({
name: "Column",
value: $scope.metric.column
});
}
if ($scope.metric.groups && $scope.metric.groups.length > 0) {
$scope.extra_table_fields.push({
name: "Groups",
value: $scope.metric.groups.join(', ')
});
}
if ($scope.metric.format) {
$scope.extra_table_fields.push({
name: "Format",
value: $scope.metric.format
});
}
// Generate usage examples - NO TEMPLATE LITERALS!
$scope.usage_examples = generateUsageExamples($scope.metric);
});
// FIXED: No template literals anywhere
function generateUsageExamples(metric) {
var examples = [];
// Basic reference example - using string concatenation
examples.push({
title: 'Basic Reference',
language: 'sql',
code: '-- Reference this metric in a model\nSELECT \n *,\n {{ calculate_metric(\'' + metric.name + '\') }} as ' + metric.name + '\nFROM {{ ref(\'' + metric.model + '\') }}'
});
// Macro usage if applicable
if (metric.method === 'macro' && metric.macro) {
examples.push({
title: 'Macro Implementation',
language: 'sql',
code: '-- Using the underlying macro\n{{ ' + metric.macro + '(\'' + metric.model + '\', \'' + metric.name + '\') }}'
});
}
// SQL implementation if provided
if (metric.sql) {
examples.push({
title: 'SQL Definition',
language: 'sql',
code: metric.sql
});
}
// Advanced usage with filters
if (metric.filters && metric.filters.length > 0) {
var filterComments = '-- ' + metric.filters.join('\n-- ');
examples.push({
title: 'With Filters Applied',
language: 'sql',
code: '-- This metric includes the following filters:\n' + filterComments + '\nSELECT {{ calculate_metric(\'' + metric.name + '\') }} as ' + metric.name + '\nFROM {{ ref(\'' + metric.model + '\') }}'
});
}
// YAML configuration example
examples.push({
title: 'YAML Configuration',
language: 'yaml',
code: generateYamlExample(metric)
});
return examples;
}
function generateYamlExample(metric) {
var yaml = '# models/' + metric.model + '.yml or in model meta\nversion: 2\nmodels:\n - name: ' + metric.model + '\n meta:\n metrics:\n ' + metric.name + ':\n';
if (metric.label && metric.label !== metric.name) {
yaml += ' label: "' + metric.label + '"\n';
}
if (metric.description) {
yaml += ' description: "' + metric.description + '"\n';
}
yaml += ' type: ' + metric.type + '\n';
yaml += ' method: ' + metric.method + '\n';
if (metric.column) {
yaml += ' column: ' + metric.column + '\n';
}
if (metric.sql) {
var indentedSql = metric.sql.replace(/\n/g, '\n ');
yaml += ' sql: |\n ' + indentedSql + '\n';
}
if (metric.macro) {
yaml += ' macro: ' + metric.macro + '\n';
}
if (metric.groups && metric.groups.length > 0) {
yaml += ' groups:\n';
for (var i = 0; i < metric.groups.length; i++) {
yaml += ' - ' + metric.groups[i] + '\n';
}
}
if (metric.filters && metric.filters.length > 0) {
yaml += ' filters:\n';
for (var j = 0; j < metric.filters.length; j++) {
yaml += ' - ' + metric.filters[j] + '\n';
}
}
return yaml;
}
$scope.copyMetricUsage = function() {
var usage = '{{ calculate_metric(\'' + $scope.metric.name + '\') }}';
codeService.copy_to_clipboard(usage);
console.log('Copied metric usage to clipboard:', usage);
};
$scope.goToModel = function() {
if ($scope.metric.model_unique_id) {
$state.go('dbt.model', {unique_id: $scope.metric.model_unique_id});
}
};
}]);
// ============================================================================= // 4. METRIC TEMPLATE // File: src/app/docs/metric.html (NEW FILE) // =============================================================================
/*
<style> .section-target { top: -8em; } .metric-badge { display: inline-block; padding: 4px 8px; border-radius: 4px; font-size: 12px; font-weight: bold; margin-right: 8px; } .metric-type-calculated { background-color: #007bff; color: white; } .metric-type-aggregate { background-color: #28a745; color: white; } .metric-type-ratio { background-color: #ffc107; color: black; } .metric-method-macro { background-color: #6f42c1; color: white; } .metric-method-sql { background-color: #17a2b8; color: white; } .usage-examples .panel { margin-bottom: 20px; } .usage-examples pre { max-height: 300px; overflow-y: auto; } </style>
{{ metric.label || metric.name }}
metric
<div class="pull-right">
<span class="metric-badge metric-type-{{ metric.type }}">{{ metric.type }}</span>
<span class="metric-badge metric-method-{{ metric.method }}">{{ metric.method }}</span>
</div>
<div class='clearfix'></div>
</h1>
<div class="metric-summary" ng-if="metric.model">
<p class="text-muted">
Defined in model
<a ng-click="goToModel()" style="cursor: pointer; color: #0094b3;">
<strong>{{ metric.model }}</strong>
</a>
<span ng-if="metric.column"> on column <code>{{ metric.column }}</code></span>
</p>
</div>
</div>
</div>
<div class="app-frame app-pad-h">
<ul class="nav nav-tabs">
<li ui-sref-active='active'><a ui-sref="dbt.metric({'#': 'details'})">Details</a></li>
<li ui-sref-active='active'><a ui-sref="dbt.metric({'#': 'description'})">Description</a></li>
<li ui-sref-active='active'><a ui-sref="dbt.metric({'#': 'usage'})">Usage</a></li>
<li ui-sref-active='active' ng-show="parentsLength != 0"><a ui-sref="dbt.metric({'#': 'depends_on'})">Depends On</a></li>
</ul>
</div>
</div>
<div class="app-details">
<div class="app-frame app-pad">
<!-- Details Section -->
<section class="section">
<div class="section-target" id="details"></div>
<table-details model="metric" extras="extra_table_fields" />
</section>
<!-- Description Section -->
<section class="section">
<div class="section-target" id="description"></div>
<div class="section-content">
<h6>Description</h6>
<div class="panel">
<div class="panel-body">
<div ng-if="metric.description" class="model-markdown" marked="metric.description"></div>
<div ng-if="!metric.description" class="text-muted">
This metric is not currently documented. Consider adding a description to help others understand its purpose and calculation.
</div>
</div>
</div>
</div>
</section>
<!-- Usage Examples Section -->
<section class="section">
<div class="section-target" id="usage"></div>
<div class="section-content">
<h6>Usage Examples</h6>
<div class="usage-examples">
<div class="panel" ng-repeat="example in usage_examples">
<div class="panel-header">
<h5 style="margin: 15px 0 10px 15px;">{{ example.title }}</h5>
</div>
<div class="panel-body">
<div class="pull-right">
<button class="btn btn-xs btn-default" ng-click="codeService.copy_to_clipboard(example.code)">
π Copy
</button>
</div>
<pre style="background-color: #f8f9fa; border: 1px solid #e9ecef; border-radius: 4px; padding: 15px;"><code class="language-{{ example.language || 'sql' }}">{{ example.code }}</code></pre>
</div>
</div>
</div>
<!-- Quick Actions -->
<div class="panel" style="background-color: #f8f9fa;">
<div class="panel-body">
<h6>Quick Actions</h6>
<div class="btn-group" role="group">
<button class="btn btn-sm btn-primary" ng-click="copyMetricUsage()">
π Copy Basic Usage
</button>
<button class="btn btn-sm btn-default" ng-click="goToModel()">
π View Source Model
</button>
</div>
</div>
</div>
</div>
</section>
<!-- SQL Implementation (if available) -->
<section class="section" ng-show="metric.sql">
<div class="section-target" id="sql"></div>
<div class="section-content">
<h6>SQL Implementation</h6>
<div class="panel">
<div class="panel-body">
<pre style="background-color: #f8f9fa; border: 1px solid #e9ecef; border-radius: 4px; padding: 15px;"><code class="language-sql">{{ metric.sql }}</code></pre>
</div>
</div>
</div>
</section>
<!-- Filters (if any) -->
<section class="section" ng-show="metric.filters.length > 0">
<div class="section-target" id="filters"></div>
<div class="section-content">
<h6>Applied Filters</h6>
<div class="panel">
<div class="panel-body">
<p>This metric applies the following filters:</p>
<ul>
<li ng-repeat="filter in metric.filters">
<code>{{ filter }}</code>
</li>
</ul>
</div>
</div>
</div>
</section>
<!-- Groups (if any) -->
<section class="section" ng-show="metric.groups.length > 0">
<div class="section-target" id="groups"></div>
<div class="section-content">
<h6>Groups</h6>
<div class="panel">
<div class="panel-body">
<p>This metric belongs to the following groups:</p>
<div>
<span ng-repeat="group in metric.groups" class="label label-info" style="margin-right: 8px;">
{{ group }}
</span>
</div>
</div>
</div>
</div>
</section>
<!-- Dependencies -->
<section class="section" ng-show="parentsLength != 0">
<div class="section-target" id="depends_on"></div>
<div class="section-content">
<h6>Depends On</h6>
<reference-list references="parents" node="metric" />
</div>
</section>
</div>
</div>
<div class="pull-right">
<span class="metric-badge metric-type-{{ metric.type }}">{{ metric.type }}</span>
<span class="metric-badge metric-method-{{ metric.method }}">{{ metric.method }}</span>
</div>
<div class='clearfix'></div>
</h1>
<div class="metric-summary" ng-if="metric.model">
<p class="text-muted">
Defined in model
<a ng-click="goToModel()" style="cursor: pointer; color: #0094b3;">
<strong>{{ metric.model }}</strong>
</a>
<span ng-if="metric.column"> on column <code>{{ metric.column }}</code></span>
</p>
</div>
</div>
</div>
<div class="app-frame app-pad-h">
<ul class="nav nav-tabs">
<li ui-sref-active='active'><a ui-sref="dbt.metric({'#': 'details'})">Details</a></li>
<li ui-sref-active='active'><a ui-sref="dbt.metric({'#': 'description'})">Description</a></li>
<li ui-sref-active='active'><a ui-sref="dbt.metric({'#': 'usage'})">Usage</a></li>
<li ui-sref-active='active' ng-show="parentsLength != 0"><a ui-sref="dbt.metric({'#': 'depends_on'})">Depends On</a></li>
</ul>
</div>
</div>
<div class="app-details">
<div class="app-frame app-pad">
<!-- Details Section -->
<section class="section">
<div class="section-target" id="details"></div>
<table-details model="metric" extras="extra_table_fields" />
</section>
<!-- Description Section -->
<section class="section">
<div class="section-target" id="description"></div>
<div class="section-content">
<h6>Description</h6>
<div class="panel">
<div class="panel-body">
<div ng-if="metric.description" class="model-markdown" marked="metric.description"></div>
<div ng-if="!metric.description" class="text-muted">
This metric is not currently documented. Consider adding a description to help others understand its purpose and calculation.
</div>
</div>
</div>
</div>
</section>
<!-- Usage Examples Section -->
<section class="section">
<div class="section-target" id="usage"></div>
<div class="section-content">
<h6>Usage Examples</h6>
<div class="usage-examples">
<div class="panel" ng-repeat="example in usage_examples">
<div class="panel-header">
<h5 style="margin: 15px 0 10px 15px;">{{ example.title }}</h5>
</div>
<div class="panel-body">
<div class="pull-right">
<button class="btn btn-xs btn-default" ng-click="codeService.copy_to_clipboard(example.code)">
π Copy
</button>
</div>
<pre style="background-color: #f8f9fa; border: 1px solid #e9ecef; border-radius: 4px; padding: 15px;"><code class="language-{{ example.language || 'sql' }}">{{ example.code }}</code></pre>
</div>
</div>
</div>
<!-- Quick Actions -->
<div class="panel" style="background-color: #f8f9fa;">
<div class="panel-body">
<h6>Quick Actions</h6>
<div class="btn-group" role="group">
<button class="btn btn-sm btn-primary" ng-click="copyMetricUsage()">
π Copy Basic Usage
</button>
<button class="btn btn-sm btn-default" ng-click="goToModel()">
π View Source Model
</button>
</div>
</div>
</div>
</div>
</section>
<!-- SQL Implementation (if available) -->
<section class="section" ng-show="metric.sql">
<div class="section-target" id="sql"></div>
<div class="section-content">
<h6>SQL Implementation</h6>
<div class="panel">
<div class="panel-body">
<pre style="background-color: #f8f9fa; border: 1px solid #e9ecef; border-radius: 4px; padding: 15px;"><code class="language-sql">{{ metric.sql }}</code></pre>
</div>
</div>
</div>
</section>
<!-- Filters (if any) -->
<section class="section" ng-show="metric.filters.length > 0">
<div class="section-target" id="filters"></div>
<div class="section-content">
<h6>Applied Filters</h6>
<div class="panel">
<div class="panel-body">
<p>This metric applies the following filters:</p>
<ul>
<li ng-repeat="filter in metric.filters">
<code>{{ filter }}</code>
</li>
</ul>
</div>
</div>
</div>
</section>
<!-- Groups (if any) -->
<section class="section" ng-show="metric.groups.length > 0">
<div class="section-target" id="groups"></div>
<div class="section-content">
<h6>Groups</h6>
<div class="panel">
<div class="panel-body">
<p>This metric belongs to the following groups:</p>
<div>
<span ng-repeat="group in metric.groups" class="label label-info" style="margin-right: 8px;">
{{ group }}
</span>
</div>
</div>
</div>
</div>
</section>
<!-- Dependencies -->
<section class="section" ng-show="parentsLength != 0">
<div class="section-target" id="depends_on"></div>
<div class="section-content">
<h6>Depends On</h6>
<reference-list references="parents" node="metric" />
</div>
</section>
</div>
</div>
// ============================================================================= // 5. MODEL CONTROLLER ENHANCEMENT // File: src/app/docs/model.js // Add after the existing model loading logic (around line 30) // =============================================================================
// Load associated metrics for this model $scope.modelMetrics = []; $scope.metricsCount = 0;
projectService.ready(function(project) { // ... existing code ...
// Find metrics associated with this model
$scope.modelMetrics = _.filter(project.nodes, function(node) {
return node.resource_type === 'metric' && node.model_unique_id === $scope.model.unique_id;
});
$scope.metricsCount = $scope.modelMetrics.length;
// Sort metrics by name for consistent display
$scope.modelMetrics = _.sortBy($scope.modelMetrics, 'name');
console.log('π Found', $scope.metricsCount, 'metrics for model', $scope.model.name);
});
// Add utility functions for metrics $scope.copyMetricUsage = function(metric) { var usage = '{{ calculate_metric('' + metric.name + '') }}'; codeService.copy_to_clipboard(usage);
// Simple feedback
console.log('Copied metric usage:', usage);
// Could enhance with toast notification here
showToast('β
Metric usage copied to clipboard!');
};
$scope.getMetricBadgeClass = function(metric) { var classes = ['label'];
switch(metric.type) {
case 'aggregate':
classes.push('label-success');
break;
case 'calculated':
classes.push('label-primary');
break;
case 'ratio':
classes.push('label-warning');
break;
default:
classes.push('label-default');
}
return classes.join(' ');
};
$scope.getMethodBadgeClass = function(metric) { return metric.method === 'macro' ? 'label label-info' : 'label label-default'; };
// Simple toast notification function
function showToast(message) {
var toast = document.createElement('div');
toast.className = 'alert alert-success';
toast.style.cssText = position: fixed; top: 20px; right: 20px; z-index: 1000; min-width: 250px; animation: slideIn 0.3s ease;
;
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(function() {
if (toast.parentNode) {
toast.style.animation = 'slideOut 0.3s ease';
setTimeout(function() {
if (toast.parentNode) {
toast.parentNode.removeChild(toast);
}
}, 300);
}
}, 3000);
}
// Add CSS for animations
var style = document.createElement('style');
style.textContent = @keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } } @keyframes slideOut { from { transform: translateX(0); opacity: 1; } to { transform: translateX(100%); opacity: 0; } }
;
document.head.appendChild(style);
// ============================================================================= // 6. MODEL TEMPLATE ENHANCEMENT // File: src/app/docs/model.html // Replace the nav tabs section and add metrics section // =============================================================================
/*
- Details
- Description
- Columns
- Metrics {{ metricsCount }}
- Referenced By
- Depends On
- Code
This model has {{ metricsCount }} metric{{ metricsCount === 1 ? '' : 's' }} defined in its configuration.
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Metric</th>
<th>Type</th>
<th>Method</th>
<th>Description</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="metric in modelMetrics">
<td style="vertical-align: middle;">
<a ui-sref="dbt.metric({unique_id: metric.unique_id})" class="text-decoration-none">
<strong>{{ metric.label || metric.name }}</strong>
</a>
<div class="text-muted small" style="margin-top: 4px;">
<code style="font-size: 11px;">{{ metric.display_name || (metric.model + '.' + metric.name) }}</code>
</div>
<div ng-if="metric.column" class="text-muted small">
Column: <code>{{ metric.column }}</code>
</div>
</td>
<td style="vertical-align: middle;">
<span ng-class="getMetricBadgeClass(metric)">{{ metric.type }}</span>
</td>
<td style="vertical-align: middle;">
<span ng-class="getMethodBadgeClass(metric)">{{ metric.method }}</span>
</td>
<td style="vertical-align: middle; max-width: 200px;">
<div ng-if="metric.description" class="text-truncate" title="{{ metric.description }}">
{{ metric.description }}
</div>
<div ng-if="!metric.description" class="text-muted">
<em>No description</em>
</div>
</td>
<td style="vertical-align: middle;">
<div class="btn-group btn-group-sm" role="group">
<button class="btn btn-default"
ng-click="copyMetricUsage(metric)"
title="Copy usage code">
π
</button>
<a class="btn btn-primary"
ui-sref="dbt.metric({unique_id: metric.unique_id})"
title="View metric details">
ποΈ
</a>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Quick usage examples -->
<div class="alert alert-info" style="margin-top: 15px;">
<strong>π‘ Quick Usage:</strong>
Use <code>{{ '{{ calculate_metric(\'METRIC_NAME\') }}' }}</code> to reference these metrics in your models.
</div>
</div>
</div>
</div>
*/
// ============================================================================= // 7. NAVIGATION UPDATES // File: src/app/components/model_tree/model_tree.html // Add after the existing metrics section // =============================================================================
/*
// ============================================================================= // 8. DOCUMENTATION INDEX UPDATE // File: src/app/docs/index.js // Add this line to include the metric controller // =============================================================================
require('./metric');
// ============================================================================= // 9. GRAPH SERVICE UPDATES // File: src/app/services/graph.service.js // Add metric styling to the style array // =============================================================================
{ selector: 'node[resource_type="metric"]', style: { 'background-color': '#ff9500', 'border-color': '#ff9500', 'shape': 'diamond' } },
// ============================================================================= // 10. SELECTOR SERVICE UPDATES // File: src/app/services/node_selection_service.js // Update initial_selector and options to include metrics // =============================================================================
var initial_selector = { include: '', exclude: '', packages: [], tags: [null], resource_types: [ 'model', 'seed', 'snapshot', 'source', 'test', 'unit_test', 'analysis', 'exposure', 'metric', // Add this line 'semantic_model', 'saved_query' ], depth: 1, };
// Update options options: { packages: [], tags: [null], resource_types: ['model', 'seed', 'snapshot', 'source', 'test', 'analysis', 'exposure', 'metric', 'semantic_model', 'unit_test', 'saved_query'], }
// ============================================================================= // 11. REFERENCES COMPONENT UPDATE // File: src/app/components/references/index.js // Add to the mapResourceType function // =============================================================================
} else if (type == 'metric') { return 'Metrics';
// ============================================================================= // EXAMPLE YAML CONFIGURATION // This is how your model YAML should be structured // =============================================================================
/* version: 2 models:
-
name: customers description: "Customer dimension table" meta: metrics: total_customers: label: "Total Customers" description: "Count of all customers" type: aggregate method: sql sql: "COUNT(*)" groups: ["core", "customer"]
avg_customer_ltv: label: "Average Customer LTV" description: "Average lifetime value per customer" type: calculated method: macro macro: calculate_avg_ltv column: lifetime_value format: "$,.2f" active_customer_rate: label: "Active Customer Rate" description: "Percentage of customers active in last 30 days" type: ratio method: sql sql: | COUNT(CASE WHEN last_order_date >= CURRENT_DATE - 30 THEN 1 END) / COUNT(*)::FLOAT format: "0.2%" filters: - "status = 'active'" groups: ["engagement"]
*/
// ============================================================================= // IMPLEMENTATION CHECKLIST // =============================================================================
/*
- β Update project_service.js with metrics extraction logic
- β Add metric route to app.js
- β Create metric controller (src/app/docs/metric.js)
- β Create metric template (src/app/docs/metric.html)
- β Update model controller to show associated metrics
- β Update model template with metrics section
- β Update navigation tree to include metrics
- β Update docs index to include metric controller
- β Update graph service for metric visualization
- β Update selector services to include metrics
- β Update references component for metrics
Testing:
- Verify metrics are extracted from model YAML
- Test navigation to metric pages
- Test model pages show associated metrics
- Test search includes metrics
- Test graph visualization includes metrics */