Display OpenMetadata catalog enrichment in table profile overview
- API endpoint /api/catalog/profile/ enriches response with catalog metadata (tier, owners, tags, url) - renderOverview() template function displays 'Data Catalog' section with tier, owners, tags, and catalog link - Graceful degradation: section only shown if catalog enrichment available
This commit is contained in:
parent
a7faf70cb3
commit
2d03a9b557
2 changed files with 70 additions and 1 deletions
|
|
@ -673,7 +673,7 @@ def register_routes(app: Flask) -> None:
|
||||||
@app.route("/api/catalog/profile/<table_name>")
|
@app.route("/api/catalog/profile/<table_name>")
|
||||||
@login_required
|
@login_required
|
||||||
def catalog_profile(table_name):
|
def catalog_profile(table_name):
|
||||||
"""Return profiler data for a single table."""
|
"""Return profiler data for a single table with OpenMetadata catalog enrichment."""
|
||||||
profiles_path = _resolve_metadata_path("profiles.json")
|
profiles_path = _resolve_metadata_path("profiles.json")
|
||||||
try:
|
try:
|
||||||
if not profiles_path.exists():
|
if not profiles_path.exists():
|
||||||
|
|
@ -686,6 +686,54 @@ def register_routes(app: Flask) -> None:
|
||||||
if not table_profile:
|
if not table_profile:
|
||||||
return jsonify({"error": f"No profile for table '{table_name}'"}), 404
|
return jsonify({"error": f"No profile for table '{table_name}'"}), 404
|
||||||
|
|
||||||
|
# Enrich with OpenMetadata catalog data if available
|
||||||
|
if _catalog_enricher and _catalog_enricher.enabled:
|
||||||
|
try:
|
||||||
|
# Find table config from data_description.md
|
||||||
|
from src.config import TableConfig
|
||||||
|
from config.loader import load_instance_config
|
||||||
|
|
||||||
|
# Load data_description.md to find table config by name
|
||||||
|
instance_config = load_instance_config()
|
||||||
|
desc_path = Path(os.path.dirname(__file__)) / ".." / "docs" / "data_description.md"
|
||||||
|
if desc_path.exists():
|
||||||
|
with open(desc_path) as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
import re
|
||||||
|
yaml_match = re.search(r'```yaml\s*\n(.*?)```', content, re.DOTALL)
|
||||||
|
if yaml_match:
|
||||||
|
import yaml
|
||||||
|
yaml_data = yaml.safe_load(yaml_match.group(1))
|
||||||
|
if yaml_data and "tables" in yaml_data:
|
||||||
|
# Find table by name
|
||||||
|
for table_def in yaml_data["tables"]:
|
||||||
|
if table_def.get("name") == table_name:
|
||||||
|
table_config = TableConfig(
|
||||||
|
id=table_def.get("id", ""),
|
||||||
|
name=table_def.get("name", ""),
|
||||||
|
description=table_def.get("description", ""),
|
||||||
|
primary_key=table_def.get("primary_key", "id"),
|
||||||
|
sync_strategy=table_def.get("sync_strategy", "full_refresh"),
|
||||||
|
catalog_fqn=table_def.get("catalog_fqn"),
|
||||||
|
)
|
||||||
|
catalog_data = _catalog_enricher.enrich_table(table_config)
|
||||||
|
if catalog_data:
|
||||||
|
# Add catalog enrichment to profile
|
||||||
|
table_profile["catalog"] = {
|
||||||
|
"description": catalog_data.description,
|
||||||
|
"tags": catalog_data.tags,
|
||||||
|
"tier": catalog_data.tier,
|
||||||
|
"owners": catalog_data.owners,
|
||||||
|
"url": catalog_data.catalog_url,
|
||||||
|
}
|
||||||
|
# Override description with catalog version
|
||||||
|
if catalog_data.description:
|
||||||
|
table_profile["description"] = catalog_data.description
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Error enriching profile for {table_name}: {e}")
|
||||||
|
|
||||||
return jsonify(table_profile)
|
return jsonify(table_profile)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error loading profile for {table_name}: {e}")
|
logger.error(f"Error loading profile for {table_name}: {e}")
|
||||||
|
|
|
||||||
|
|
@ -1723,6 +1723,26 @@ function renderOverview(data) {
|
||||||
const dr = data.date_range;
|
const dr = data.date_range;
|
||||||
const dateRow = dr ? `<tr><td>Date Coverage</td><td>${dr.earliest} to ${dr.latest}</td></tr>` : '';
|
const dateRow = dr ? `<tr><td>Date Coverage</td><td>${dr.earliest} to ${dr.latest}</td></tr>` : '';
|
||||||
|
|
||||||
|
// Catalog enrichment section
|
||||||
|
let catalogHtml = '';
|
||||||
|
if (data.catalog) {
|
||||||
|
const cat = data.catalog;
|
||||||
|
const tagsHtml = (cat.tags || []).length > 0
|
||||||
|
? `<div style="margin-top:8px;"><span style="font-size:11px;color:var(--text-secondary);font-weight:500;">Tags:</span> ${cat.tags.map(t => `<span class="metric-badge" style="display:inline-block;margin:2px 4px 2px 0;">${t}</span>`).join('')}</div>`
|
||||||
|
: '';
|
||||||
|
const ownersHtml = (cat.owners || []).length > 0
|
||||||
|
? `<div style="margin-top:8px;"><span style="font-size:11px;color:var(--text-secondary);font-weight:500;">Owners:</span> ${cat.owners.join(', ')}</div>`
|
||||||
|
: '';
|
||||||
|
const tierHtml = cat.tier
|
||||||
|
? `<div style="margin-top:8px;"><span style="font-size:11px;color:var(--text-secondary);font-weight:500;">Tier:</span> <strong>${cat.tier}</strong></div>`
|
||||||
|
: '';
|
||||||
|
const urlHtml = cat.url
|
||||||
|
? `<div style="margin-top:8px;"><a href="${cat.url}" target="_blank" style="font-size:11px;color:var(--primary);text-decoration:none;">View in Data Catalog →</a></div>`
|
||||||
|
: '';
|
||||||
|
|
||||||
|
catalogHtml = `<div style="margin-top:16px;border-top:1px solid var(--border);padding-top:16px;"><div class="overview-title">Data Catalog</div>${tierHtml}${ownersHtml}${tagsHtml}${urlHtml}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
document.getElementById('sectionOverview').innerHTML = `
|
document.getElementById('sectionOverview').innerHTML = `
|
||||||
<div class="overview-grid">
|
<div class="overview-grid">
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -1743,6 +1763,7 @@ function renderOverview(data) {
|
||||||
<table class="overview-stats-table">${vtHtml}</table>
|
<table class="overview-stats-table">${vtHtml}</table>
|
||||||
${data.description ? `<div style="margin-top:16px;"><div class="overview-title">Description</div><p style="font-size:13px;color:var(--text-secondary);">${data.description}</p></div>` : ''}
|
${data.description ? `<div style="margin-top:16px;"><div class="overview-title">Description</div><p style="font-size:13px;color:var(--text-secondary);">${data.description}</p></div>` : ''}
|
||||||
${(data.used_by_metrics || []).length > 0 ? `<div style="margin-top:16px;"><div class="overview-title">Used by Metrics</div><div>${data.used_by_metrics.map(m => m.file ? `<a class="metric-badge" style="cursor:pointer;text-decoration:none;" onclick="openMetricModal('${m.file}')">${m.name}</a>` : `<span class="metric-badge">${typeof m === 'string' ? m : m.name}</span>`).join('')}</div></div>` : ''}
|
${(data.used_by_metrics || []).length > 0 ? `<div style="margin-top:16px;"><div class="overview-title">Used by Metrics</div><div>${data.used_by_metrics.map(m => m.file ? `<a class="metric-badge" style="cursor:pointer;text-decoration:none;" onclick="openMetricModal('${m.file}')">${m.name}</a>` : `<span class="metric-badge">${typeof m === 'string' ? m : m.name}</span>`).join('')}</div></div>` : ''}
|
||||||
|
${catalogHtml}
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue