@clickzetta/cz-cli-darwin-arm64 0.3.40 → 0.3.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cz-cli +0 -0
- package/bin/skills/clickzetta-app-python-sdk/SKILL.md +153 -0
- package/bin/skills/clickzetta-app-python-sdk/eval_cases.jsonl +12 -0
- package/bin/skills/clickzetta-app-python-sdk/references/bulkload.md +196 -0
- package/bin/skills/clickzetta-app-python-sdk/references/connector.md +143 -0
- package/bin/skills/clickzetta-app-python-sdk/references/realtime.md +122 -0
- package/bin/skills/clickzetta-batch-sync-pipeline/SKILL.md +128 -287
- package/bin/skills/clickzetta-bi-connect/SKILL.md +176 -0
- package/bin/skills/clickzetta-bi-connect/eval_cases.jsonl +5 -0
- package/bin/skills/clickzetta-bi-connect/references/bi-tools.md +170 -0
- package/bin/skills/clickzetta-cdc-sync-pipeline/SKILL.md +633 -0
- package/bin/skills/clickzetta-cdc-sync-pipeline/eval_cases.jsonl +5 -0
- package/bin/skills/clickzetta-data-ingest-pipeline/eval_cases.jsonl +5 -0
- package/bin/skills/clickzetta-data-science/SKILL.md +125 -0
- package/bin/skills/clickzetta-data-science/eval_cases.jsonl +12 -0
- package/bin/skills/clickzetta-data-science/references/bitmap-profile.md +146 -0
- package/bin/skills/clickzetta-data-science/references/data-patterns.md +110 -0
- package/bin/skills/clickzetta-data-science/references/setup.md +160 -0
- package/bin/skills/clickzetta-data-science/references/stats-functions.md +195 -0
- package/bin/skills/clickzetta-data-science/references/write-and-infer.md +122 -0
- package/bin/skills/clickzetta-data-science/references/zettapark-api.md +156 -0
- package/bin/skills/clickzetta-data-sharing/SKILL.md +160 -0
- package/bin/skills/clickzetta-data-sharing/eval_cases.jsonl +3 -0
- package/bin/skills/clickzetta-data-sharing/references/share-ddl.md +134 -0
- package/bin/skills/clickzetta-dw-modeling/SKILL.md +103 -11
- package/bin/skills/clickzetta-dynamic-table/SKILL.md +58 -2
- package/bin/skills/clickzetta-dynamic-table/dynamic-table-alter/SKILL.md +4 -4
- package/bin/skills/clickzetta-external-catalog/SKILL.md +123 -0
- package/bin/skills/clickzetta-external-catalog/eval_cases.jsonl +5 -0
- package/bin/skills/clickzetta-external-catalog/references/external-catalog-ddl.md +130 -0
- package/bin/skills/clickzetta-file-import-pipeline/SKILL.md +34 -0
- package/bin/skills/clickzetta-java-sdk/SKILL.md +186 -0
- package/bin/skills/clickzetta-java-sdk/eval_cases.jsonl +12 -0
- package/bin/skills/clickzetta-java-sdk/references/bulkload.md +163 -0
- package/bin/skills/clickzetta-java-sdk/references/realtime.md +212 -0
- package/bin/skills/clickzetta-kafka-ingest-pipeline/SKILL.md +31 -0
- package/bin/skills/clickzetta-metadata/SKILL.md +28 -30
- package/bin/skills/clickzetta-oss-ingest-pipeline/SKILL.md +39 -0
- package/bin/skills/clickzetta-pipeline-review/SKILL.md +377 -0
- package/bin/skills/clickzetta-realtime-sync-pipeline/SKILL.md +323 -0
- package/bin/skills/clickzetta-realtime-sync-pipeline/eval_cases.jsonl +5 -0
- package/bin/skills/clickzetta-semantic-view/SKILL.md +207 -0
- package/bin/skills/clickzetta-semantic-view/eval_cases.jsonl +12 -0
- package/bin/skills/clickzetta-semantic-view/references/semantic-view-reference.md +167 -0
- package/bin/skills/clickzetta-spark-flink-connector/SKILL.md +92 -0
- package/bin/skills/clickzetta-spark-flink-connector/eval_cases.jsonl +5 -0
- package/bin/skills/clickzetta-spark-flink-connector/references/flink.md +147 -0
- package/bin/skills/clickzetta-spark-flink-connector/references/spark.md +132 -0
- package/bin/skills/clickzetta-sql-pipeline-manager/SKILL.md +115 -9
- package/bin/skills/clickzetta-sql-syntax-guide/SKILL.md +249 -0
- package/bin/skills/clickzetta-sql-syntax-guide/eval_cases.jsonl +3 -0
- package/bin/skills/clickzetta-sql-syntax-guide/references/ddl-reference.md +350 -0
- package/bin/skills/clickzetta-sql-syntax-guide/references/dml-reference.md +279 -0
- package/bin/skills/clickzetta-sql-syntax-guide/references/dql-reference.md +504 -0
- package/bin/skills/clickzetta-sql-syntax-guide/references/functions-reference.md +372 -0
- package/bin/skills/clickzetta-sql-syntax-guide/references/migration-databricks.md +260 -0
- package/bin/skills/clickzetta-sql-syntax-guide/references/migration-snowflake.md +382 -0
- package/bin/skills/clickzetta-sql-syntax-guide/references/vs-snowflake.md +346 -0
- package/bin/skills/clickzetta-sql-syntax-guide/references/vs-spark.md +229 -0
- package/bin/skills/clickzetta-studio-task-manager/SKILL.md +652 -0
- package/bin/skills/clickzetta-table-lineage/SKILL.md +90 -0
- package/bin/skills/clickzetta-table-lineage/eval_cases.jsonl +1 -0
- package/bin/skills/clickzetta-table-lineage/references/normalize_func.sql +14 -0
- package/bin/skills/clickzetta-table-lineage/references/table_cost.sql +38 -0
- package/bin/skills/clickzetta-table-lineage/references/table_lineage_standalone.html +562 -0
- package/bin/skills/clickzetta-table-lineage/references/table_relation.sql +25 -0
- package/bin/skills/clickzetta-zettapark/SKILL.md +248 -0
- package/bin/skills/clickzetta-zettapark/eval_cases.jsonl +12 -0
- package/bin/skills/clickzetta-zettapark/references/zettapark-api.md +283 -0
- package/package.json +1 -1
- package/bin/skills/clickzetta-ai-vector-search/SKILL.md +0 -160
- package/bin/skills/clickzetta-ai-vector-search/eval_cases.jsonl +0 -4
- package/bin/skills/clickzetta-ai-vector-search/references/vector-search.md +0 -155
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Table Lineage Graph</title>
|
|
7
|
+
<style>
|
|
8
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
9
|
+
:root, html[data-theme="light"] {
|
|
10
|
+
--bg: #f5f7fa; --surface: #ffffff; --surface2: #f0f1f4;
|
|
11
|
+
--border: #d8dce6; --text: #1e2330; --text2: #6b7280;
|
|
12
|
+
--accent: #4f46e5; --accent2: #6366f1;
|
|
13
|
+
--red: #dc2626; --orange: #d97706; --green: #16a34a;
|
|
14
|
+
--purple: #9333ea; --teal: #0d9488; --gray: #9ca3af;
|
|
15
|
+
--shadow: rgba(0,0,0,0.08);
|
|
16
|
+
}
|
|
17
|
+
html[data-theme="dark"] {
|
|
18
|
+
--bg: #0f1117; --surface: #1a1d27; --surface2: #242836;
|
|
19
|
+
--border: #2e3348; --text: #e1e4ed; --text2: #8b90a0;
|
|
20
|
+
--accent: #6366f1; --accent2: #818cf8;
|
|
21
|
+
--red: #ef4444; --orange: #f59e0b; --green: #22c55e;
|
|
22
|
+
--purple: #a855f7; --teal: #14b8a6; --gray: #6b7280;
|
|
23
|
+
--shadow: rgba(0,0,0,0.3);
|
|
24
|
+
}
|
|
25
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', sans-serif; background: var(--bg); color: var(--text); overflow: hidden; height: 100vh; }
|
|
26
|
+
#topbar { display: flex; align-items: center; gap: 12px; padding: 10px 20px; background: var(--surface); border-bottom: 1px solid var(--border); z-index: 100; position: relative; }
|
|
27
|
+
#topbar h1 { font-size: 15px; font-weight: 600; white-space: nowrap; }
|
|
28
|
+
#topbar .logo { display: flex; align-items: center; gap: 8px; }
|
|
29
|
+
#topbar .logo svg { width: 22px; height: 22px; color: var(--accent); }
|
|
30
|
+
.tb-sep { width: 1px; height: 24px; background: var(--border); }
|
|
31
|
+
#search-box { display: flex; align-items: center; gap: 6px; background: var(--surface2); border: 1px solid var(--border); border-radius: 8px; padding: 5px 10px; min-width: 240px; transition: border-color 0.2s; }
|
|
32
|
+
#search-box:focus-within { border-color: var(--accent); }
|
|
33
|
+
#search-box svg { width: 14px; height: 14px; color: var(--text2); flex-shrink: 0; }
|
|
34
|
+
#search-input { background: none; border: none; outline: none; color: var(--text); font-size: 13px; width: 100%; }
|
|
35
|
+
#search-input::placeholder { color: var(--text2); }
|
|
36
|
+
#search-count { font-size: 11px; color: var(--text2); white-space: nowrap; }
|
|
37
|
+
.tb-btn { display: flex; align-items: center; gap: 5px; padding: 5px 12px; border-radius: 7px; border: 1px solid var(--border); background: var(--surface2); color: var(--text2); cursor: pointer; font-size: 12px; font-weight: 500; transition: all 0.15s; white-space: nowrap; }
|
|
38
|
+
.tb-btn:hover { background: var(--border); color: var(--text); }
|
|
39
|
+
.tb-btn.active { background: var(--accent); color: white; border-color: var(--accent); }
|
|
40
|
+
.tb-btn svg { width: 14px; height: 14px; }
|
|
41
|
+
#stats { margin-left: auto; font-size: 12px; color: var(--text2); white-space: nowrap; }
|
|
42
|
+
#stats .warn { color: var(--red); font-weight: 600; }
|
|
43
|
+
#canvas { width: 100%; height: calc(100vh - 46px); position: relative; overflow: hidden; cursor: grab; }
|
|
44
|
+
#canvas:active { cursor: grabbing; }
|
|
45
|
+
#canvas svg { width: 100%; height: 100%; }
|
|
46
|
+
.node-card { position: absolute; border-radius: 10px; border: 1.5px solid var(--border); background: var(--surface); padding: 0; cursor: pointer; transition: border-color 0.2s, box-shadow 0.2s, opacity 0.3s; min-width: 360px; user-select: none; }
|
|
47
|
+
.node-card:hover { border-color: var(--accent2); box-shadow: 0 0 0 1px var(--accent2), 0 4px 20px rgba(99,102,241,0.15); z-index: 10; }
|
|
48
|
+
.node-card.highlighted { border-color: var(--orange); box-shadow: 0 0 0 2px var(--orange), 0 4px 24px rgba(245,158,11,0.25); z-index: 20; }
|
|
49
|
+
.node-card.highlighted-down { border-color: #06b6d4; box-shadow: 0 0 0 2px #06b6d4, 0 4px 24px rgba(6,182,212,0.25); z-index: 15; }
|
|
50
|
+
.node-card.selected { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent), 0 4px 24px rgba(99,102,241,0.3); z-index: 30; }
|
|
51
|
+
.node-card.dimmed { opacity: 0.12; pointer-events: none; }
|
|
52
|
+
.node-card.search-match { border-color: var(--green); box-shadow: 0 0 0 1px var(--green), 0 2px 12px rgba(34,197,94,0.2); }
|
|
53
|
+
.node-header { display: flex; align-items: center; gap: 6px; padding: 8px 12px; border-bottom: 1px solid var(--border); border-radius: 10px 10px 0 0; }
|
|
54
|
+
.node-type-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
|
|
55
|
+
.node-title { font-size: 12px; font-weight: 600; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 400px; }
|
|
56
|
+
.node-badge { margin-left: auto; font-size: 9px; font-weight: 700; padding: 1px 6px; border-radius: 4px; text-transform: uppercase; letter-spacing: 0.5px; flex-shrink: 0; }
|
|
57
|
+
.node-body { padding: 6px 12px 8px; }
|
|
58
|
+
.node-metric { display: flex; justify-content: space-between; align-items: center; padding: 2px 0; }
|
|
59
|
+
.node-metric-label { font-size: 10px; color: var(--text2); }
|
|
60
|
+
.node-metric-value { font-size: 11px; font-weight: 600; font-variant-numeric: tabular-nums; }
|
|
61
|
+
.node-metric-value.acc { color: var(--orange); }
|
|
62
|
+
.port { position: absolute; width: 8px; height: 8px; border-radius: 50%; background: var(--border); }
|
|
63
|
+
.port-in { left: -4px; top: 50%; transform: translateY(-50%); }
|
|
64
|
+
.port-out { right: -4px; top: 50%; transform: translateY(-50%); }
|
|
65
|
+
#minimap { position: absolute; right: 16px; bottom: 16px; width: 180px; height: 120px; background: var(--surface); border: 1px solid var(--border); border-radius: 8px; overflow: hidden; z-index: 50; }
|
|
66
|
+
#minimap canvas { width: 100%; height: 100%; }
|
|
67
|
+
.minimap-viewport { position: absolute; border: 1.5px solid var(--accent); background: rgba(99,102,241,0.08); border-radius: 2px; cursor: move; pointer-events: all; }
|
|
68
|
+
#tooltip { position: fixed; padding: 12px 16px; background: var(--surface2); border: 1px solid var(--border); border-radius: 10px; pointer-events: none; font-size: 12px; line-height: 1.7; display: none; z-index: 9999; max-width: 420px; box-shadow: 0 8px 32px var(--shadow); backdrop-filter: blur(8px); }
|
|
69
|
+
.tt-name { font-weight: 600; font-size: 13px; color: var(--accent2); word-break: break-all; margin-bottom: 6px; }
|
|
70
|
+
.tt-row { display: flex; justify-content: space-between; gap: 20px; }
|
|
71
|
+
.tt-label { color: var(--text2); } .tt-value { font-weight: 600; text-align: right; font-variant-numeric: tabular-nums; }
|
|
72
|
+
.tt-sep { border-top: 1px solid var(--border); margin: 4px 0; }
|
|
73
|
+
.tt-acc { color: var(--orange); } .tt-type { font-weight: 600; margin-bottom: 4px; }
|
|
74
|
+
#legend { position: absolute; bottom: 16px; left: 16px; display: flex; align-items: center; gap: 14px; padding: 8px 14px; background: var(--surface); border: 1px solid var(--border); border-radius: 8px; font-size: 11px; color: var(--text2); z-index: 50; flex-wrap: wrap; }
|
|
75
|
+
.leg-dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; margin-right: 4px; vertical-align: middle; }
|
|
76
|
+
.leg-item { display: flex; align-items: center; gap: 3px; white-space: nowrap; }
|
|
77
|
+
#edges-svg { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 1; }
|
|
78
|
+
#edges-svg path { fill: none; stroke-width: 1.5; transition: stroke 0.3s, opacity 0.3s; }
|
|
79
|
+
#edges-svg path.edge-normal { stroke: var(--border); }
|
|
80
|
+
#edges-svg path.edge-bidirectional { stroke: var(--red); stroke-width: 2; stroke-dasharray: 6 3; }
|
|
81
|
+
#edges-svg path.edge-highlighted { stroke: var(--orange); stroke-width: 2.5; opacity: 1 !important; }
|
|
82
|
+
#edges-svg path.edge-highlighted-down { stroke: #06b6d4; stroke-width: 2; opacity: 1 !important; }
|
|
83
|
+
#edges-svg path.edge-dimmed { opacity: 0.06; }
|
|
84
|
+
#edges-svg marker { overflow: visible; }
|
|
85
|
+
/* Upload overlay */
|
|
86
|
+
#upload-overlay { position: fixed; inset: 0; background: var(--bg); z-index: 200; display: flex; align-items: center; justify-content: center; }
|
|
87
|
+
#upload-box { background: var(--surface); border: 2px dashed var(--border); border-radius: 16px; padding: 48px 56px; text-align: center; max-width: 520px; width: 90%; }
|
|
88
|
+
#upload-box h2 { font-size: 22px; margin-bottom: 8px; }
|
|
89
|
+
#upload-box p { color: var(--text2); font-size: 13px; margin-bottom: 24px; line-height: 1.6; }
|
|
90
|
+
#upload-box .file-row { display: flex; align-items: center; gap: 10px; margin-bottom: 14px; }
|
|
91
|
+
#upload-box label { font-size: 13px; font-weight: 600; min-width: 120px; text-align: right; }
|
|
92
|
+
#upload-box input[type=file] { font-size: 13px; color: var(--text); }
|
|
93
|
+
#upload-box .go-btn { margin-top: 20px; padding: 10px 32px; border-radius: 8px; border: none; background: var(--accent); color: white; font-size: 14px; font-weight: 600; cursor: pointer; transition: background 0.15s; }
|
|
94
|
+
#upload-box .go-btn:hover { background: var(--accent2); }
|
|
95
|
+
#upload-box .go-btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
96
|
+
#upload-box .err { color: var(--red); font-size: 12px; margin-top: 10px; min-height: 18px; }
|
|
97
|
+
#upload-box .hint { color: var(--text2); font-size: 11px; margin-top: 16px; line-height: 1.5; }
|
|
98
|
+
</style>
|
|
99
|
+
</head>
|
|
100
|
+
<body>
|
|
101
|
+
<div id="upload-overlay">
|
|
102
|
+
<div id="upload-box">
|
|
103
|
+
<h2>📊 Table Lineage Viewer</h2>
|
|
104
|
+
<p>Select your CSV files to visualize the table dependency graph.<br>All processing happens locally in your browser.</p>
|
|
105
|
+
<div class="file-row"><label>table_relation.csv</label><input type="file" id="file-relation" accept=".csv"></div>
|
|
106
|
+
<div class="file-row"><label>table_cost.csv</label><input type="file" id="file-cost" accept=".csv"></div>
|
|
107
|
+
<button class="go-btn" id="go-btn" onclick="loadFiles()">Visualize</button>
|
|
108
|
+
<div class="err" id="upload-err"></div>
|
|
109
|
+
<div class="hint">Relation CSV: table_name, upstream<br>Cost CSV: table_name, dml_cru, dml_job_cnt, query_cru, query_job_cnt<br>Cost file is optional.</div>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
<div id="topbar" style="display:none">
|
|
113
|
+
<div class="logo">
|
|
114
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>
|
|
115
|
+
<h1>Table Lineage</h1>
|
|
116
|
+
</div>
|
|
117
|
+
<span class="tb-sep"></span>
|
|
118
|
+
<button class="tb-btn" onclick="showUpload()" title="Load new files">
|
|
119
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
|
|
120
|
+
Load
|
|
121
|
+
</button>
|
|
122
|
+
<span class="tb-sep"></span>
|
|
123
|
+
<div id="search-box">
|
|
124
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
|
|
125
|
+
<input id="search-input" placeholder="Search tables..." autocomplete="off" spellcheck="false">
|
|
126
|
+
<span id="search-count"></span>
|
|
127
|
+
</div>
|
|
128
|
+
<span class="tb-sep"></span>
|
|
129
|
+
<button class="tb-btn" onclick="zoomIn()">+</button>
|
|
130
|
+
<button class="tb-btn" onclick="zoomOut()">−</button>
|
|
131
|
+
<button class="tb-btn" onclick="zoomFit()">Fit</button>
|
|
132
|
+
<button class="tb-btn active" onclick="clearHighlight()">Clear</button>
|
|
133
|
+
<button class="tb-btn" id="theme-toggle" onclick="toggleTheme()">
|
|
134
|
+
<svg id="theme-icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display:none"><circle cx="12" cy="12" r="5"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/></svg>
|
|
135
|
+
<svg id="theme-icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
|
|
136
|
+
</button>
|
|
137
|
+
<span id="stats"></span>
|
|
138
|
+
</div>
|
|
139
|
+
<div id="canvas" style="display:none">
|
|
140
|
+
<svg id="edges-svg"><defs>
|
|
141
|
+
<marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z" fill="var(--border)"/></marker>
|
|
142
|
+
<marker id="arrow-hl" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z" fill="var(--orange)"/></marker>
|
|
143
|
+
<marker id="arrow-bi" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z" fill="var(--red)"/></marker>
|
|
144
|
+
<marker id="arrow-down" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z" fill="#06b6d4"/></marker>
|
|
145
|
+
</defs></svg>
|
|
146
|
+
<div id="nodes-container" style="position:absolute;top:0;left:0;z-index:2;"></div>
|
|
147
|
+
</div>
|
|
148
|
+
<div id="legend" style="display:none">
|
|
149
|
+
<span class="leg-item"><span class="leg-dot" style="background:var(--red)"></span>CRU/day>1000</span>
|
|
150
|
+
<span class="leg-item"><span class="leg-dot" style="background:var(--orange)"></span>100–1000</span>
|
|
151
|
+
<span class="leg-item"><span class="leg-dot" style="background:var(--green)"></span><100</span>
|
|
152
|
+
<span class="leg-item"><span class="leg-dot" style="background:var(--gray)"></span>No data</span>
|
|
153
|
+
<span style="color:var(--border)">│</span>
|
|
154
|
+
<span class="leg-item"><span class="leg-dot" style="background:var(--purple)"></span>Kafka</span>
|
|
155
|
+
<span class="leg-item"><span class="leg-dot" style="background:var(--teal)"></span>Volume</span>
|
|
156
|
+
<span style="color:var(--border)">│</span>
|
|
157
|
+
<span style="color:var(--red)">━━</span> Bidirectional
|
|
158
|
+
<span style="color:var(--border)">│</span>
|
|
159
|
+
<span>Click node: <span style="color:var(--orange)">■</span> upstream <span style="color:#06b6d4">■</span> downstream</span>
|
|
160
|
+
</div>
|
|
161
|
+
<div id="minimap" style="display:none"><canvas id="minimap-canvas"></canvas><div class="minimap-viewport" id="minimap-vp"></div></div>
|
|
162
|
+
<div id="tooltip"></div>
|
|
163
|
+
<!-- Data injection point: include a JS file that sets window.LINEAGE_DATA before this script -->
|
|
164
|
+
<!-- e.g. <script src="lineage_data.js"></script> or inline <script>window.LINEAGE_DATA={relation:"...",cost:"..."};</script> -->
|
|
165
|
+
<script>
|
|
166
|
+
'use strict';
|
|
167
|
+
// --- Theme (runs immediately) ---
|
|
168
|
+
function applyTheme(t){document.documentElement.setAttribute('data-theme',t);localStorage.setItem('lineage-theme',t);var s=document.getElementById('theme-icon-sun'),m=document.getElementById('theme-icon-moon');if(s&&m){if(t==='dark'){s.style.display='none';m.style.display='';}else{s.style.display='';m.style.display='none';}}}
|
|
169
|
+
window.toggleTheme=function(){var c=document.documentElement.getAttribute('data-theme')||'light';applyTheme(c==='dark'?'light':'dark');};
|
|
170
|
+
applyTheme(localStorage.getItem('lineage-theme')||'light');
|
|
171
|
+
|
|
172
|
+
// --- CSV parsing ---
|
|
173
|
+
function parseCSV(text){
|
|
174
|
+
var lines=text.replace(/\r\n/g,'\n').replace(/\r/g,'\n').split('\n');
|
|
175
|
+
if(lines.length<2)return[];
|
|
176
|
+
var hdr=parseCSVLine(lines[0]);
|
|
177
|
+
var rows=[];
|
|
178
|
+
for(var i=1;i<lines.length;i++){
|
|
179
|
+
var vals=parseCSVLine(lines[i]);
|
|
180
|
+
if(vals.length<hdr.length)continue;
|
|
181
|
+
var obj={};
|
|
182
|
+
for(var j=0;j<hdr.length;j++) obj[hdr[j].trim().replace(/^\uFEFF/,'')]=vals[j];
|
|
183
|
+
rows.push(obj);
|
|
184
|
+
}
|
|
185
|
+
return rows;
|
|
186
|
+
}
|
|
187
|
+
function parseCSVLine(line){
|
|
188
|
+
var result=[],cur='',inQ=false;
|
|
189
|
+
for(var i=0;i<line.length;i++){
|
|
190
|
+
var c=line[i];
|
|
191
|
+
if(inQ){if(c==='"'){if(i+1<line.length&&line[i+1]==='"'){cur+='"';i++;}else inQ=false;}else cur+=c;}
|
|
192
|
+
else{if(c==='"')inQ=true;else if(c===','){result.push(cur);cur='';}else cur+=c;}
|
|
193
|
+
}
|
|
194
|
+
result.push(cur);
|
|
195
|
+
return result;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// --- Build graph from parsed CSV rows ---
|
|
199
|
+
function buildGraph(relRows, costRows){
|
|
200
|
+
var edges=[], costs={};
|
|
201
|
+
// Parse relations
|
|
202
|
+
relRows.forEach(function(r){
|
|
203
|
+
var tn=(r.table_name||'').trim(), up=(r.upstream||'').trim();
|
|
204
|
+
if(!up)return;
|
|
205
|
+
up.split(',').forEach(function(u){
|
|
206
|
+
u=u.trim(); if(u&&u!==tn) edges.push({source:u,target:tn});
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
// Parse costs
|
|
210
|
+
(costRows||[]).forEach(function(r){
|
|
211
|
+
var tn=(r.table_name||'').trim();
|
|
212
|
+
if(!tn)return;
|
|
213
|
+
function cl(v){v=(v||'').trim().replace(/,/g,'');return v?parseFloat(v)||0:0;}
|
|
214
|
+
costs[tn]={dml_cru:cl(r.dml_cru),dml_job_cnt:Math.round(cl(r.dml_job_cnt)),query_cru:cl(r.query_cru),query_job_cnt:Math.round(cl(r.query_job_cnt))};
|
|
215
|
+
});
|
|
216
|
+
// Collect tables
|
|
217
|
+
var tSet={};
|
|
218
|
+
edges.forEach(function(e){tSet[e.source]=1;tSet[e.target]=1;});
|
|
219
|
+
var tableNames=Object.keys(tSet);
|
|
220
|
+
// Upstream map
|
|
221
|
+
var upMap={};
|
|
222
|
+
tableNames.forEach(function(t){upMap[t]=[];});
|
|
223
|
+
edges.forEach(function(e){if(upMap[e.target])upMap[e.target].push(e.source);});
|
|
224
|
+
// Accumulated cost via upstream sets
|
|
225
|
+
var usCache={};
|
|
226
|
+
function getUS(id){
|
|
227
|
+
if(usCache[id])return usCache[id];
|
|
228
|
+
var res={},stack=[id],vis={};
|
|
229
|
+
while(stack.length){
|
|
230
|
+
var n=stack.pop();
|
|
231
|
+
if(usCache[n]){for(var k in usCache[n])res[k]=1;continue;}
|
|
232
|
+
if(vis[n]){res[n]=1;continue;}
|
|
233
|
+
if(res[n])continue;
|
|
234
|
+
vis[n]=1;res[n]=1;
|
|
235
|
+
(upMap[n]||[]).forEach(function(u){if(usCache[u]){for(var k in usCache[u])res[k]=1;}else if(!res[u])stack.push(u);});
|
|
236
|
+
}
|
|
237
|
+
usCache[id]=res;return res;
|
|
238
|
+
}
|
|
239
|
+
var accCosts={};
|
|
240
|
+
tableNames.forEach(function(t){
|
|
241
|
+
var us=getUS(t),sum=0;
|
|
242
|
+
for(var k in us)sum+=(costs[k]||{}).dml_cru||0;
|
|
243
|
+
accCosts[t]=sum;
|
|
244
|
+
});
|
|
245
|
+
// Build nodes
|
|
246
|
+
var nodes=[];
|
|
247
|
+
tableNames.sort().forEach(function(t){
|
|
248
|
+
var c=costs[t]||{}, parts=t.split('.'), short=parts.length>=2?parts.slice(-2).join('.'):t;
|
|
249
|
+
var tt=t.startsWith('KAFKA.')?'kafka':t.startsWith('VOLUME.')?'volume':'normal';
|
|
250
|
+
nodes.push({id:t,name:t,shortName:short,type:tt,dml_cru:c.dml_cru||0,dml_job_cnt:c.dml_job_cnt||0,query_cru:c.query_cru||0,query_job_cnt:c.query_job_cnt||0,accumulated_cost:accCosts[t]||0});
|
|
251
|
+
});
|
|
252
|
+
// Bidirectional detection
|
|
253
|
+
var eSet={};
|
|
254
|
+
edges.forEach(function(e){eSet[e.source+'|'+e.target]=1;});
|
|
255
|
+
var biPairs={};
|
|
256
|
+
edges.forEach(function(e){if(eSet[e.target+'|'+e.source]){var p=[e.source,e.target].sort().join('|');biPairs[p]=1;}});
|
|
257
|
+
var links=[];
|
|
258
|
+
edges.forEach(function(e){var p=[e.source,e.target].sort().join('|');links.push({source:e.source,target:e.target,bidirectional:!!biPairs[p]});});
|
|
259
|
+
return {nodes:nodes,links:links};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// --- Layout + render ---
|
|
263
|
+
var LAYER_GAP=560, NODE_H=76, NODE_GAP=16, NODE_W=400;
|
|
264
|
+
var G={}; // global state for interactions
|
|
265
|
+
|
|
266
|
+
function renderGraph(NODES,LINKS){
|
|
267
|
+
// Reset
|
|
268
|
+
var container=document.getElementById('nodes-container');
|
|
269
|
+
container.innerHTML='';
|
|
270
|
+
var edgesSvg=document.getElementById('edges-svg');
|
|
271
|
+
var oldG=edgesSvg.querySelector('g.edges-group');
|
|
272
|
+
if(oldG)oldG.remove();
|
|
273
|
+
var canvas=document.getElementById('canvas');
|
|
274
|
+
|
|
275
|
+
var nodeMap={};
|
|
276
|
+
NODES.forEach(function(n){nodeMap[n.id]=n;});
|
|
277
|
+
var childrenOf={},parentsOf={};
|
|
278
|
+
NODES.forEach(function(n){childrenOf[n.id]=[];parentsOf[n.id]=[];});
|
|
279
|
+
LINKS.forEach(function(l){childrenOf[l.source].push(l.target);parentsOf[l.target].push(l.source);});
|
|
280
|
+
|
|
281
|
+
// Layers via longest-path
|
|
282
|
+
var layers={};
|
|
283
|
+
function assignLayer(id,vis){
|
|
284
|
+
if(layers[id]!==undefined)return layers[id];
|
|
285
|
+
if(!vis)vis={};if(vis[id])return 0;vis[id]=true;
|
|
286
|
+
var mx=-1;
|
|
287
|
+
(parentsOf[id]||[]).forEach(function(p){mx=Math.max(mx,assignLayer(p,Object.assign({},vis)));});
|
|
288
|
+
layers[id]=mx+1;return layers[id];
|
|
289
|
+
}
|
|
290
|
+
NODES.forEach(function(n){assignLayer(n.id);});
|
|
291
|
+
var maxLayer=0;
|
|
292
|
+
NODES.forEach(function(n){if(layers[n.id]>maxLayer)maxLayer=layers[n.id];});
|
|
293
|
+
|
|
294
|
+
// Group by layer (for global maxLayer)
|
|
295
|
+
var lg={};
|
|
296
|
+
NODES.forEach(function(n){var l=layers[n.id];if(!lg[l])lg[l]=[];lg[l].push(n);});
|
|
297
|
+
|
|
298
|
+
// Detect connected components (subgraphs) via BFS
|
|
299
|
+
var visited={}, components=[];
|
|
300
|
+
NODES.forEach(function(n){
|
|
301
|
+
if(visited[n.id])return;
|
|
302
|
+
var comp=[],queue=[n.id];
|
|
303
|
+
while(queue.length){
|
|
304
|
+
var cur=queue.shift();
|
|
305
|
+
if(visited[cur])continue;
|
|
306
|
+
visited[cur]=true;comp.push(cur);
|
|
307
|
+
(childrenOf[cur]||[]).forEach(function(c){if(!visited[c])queue.push(c);});
|
|
308
|
+
(parentsOf[cur]||[]).forEach(function(p){if(!visited[p])queue.push(p);});
|
|
309
|
+
}
|
|
310
|
+
components.push(comp);
|
|
311
|
+
});
|
|
312
|
+
// Sort components: largest first
|
|
313
|
+
components.sort(function(a,b){return b.length-a.length;});
|
|
314
|
+
|
|
315
|
+
// Position each subgraph independently, stacked vertically
|
|
316
|
+
var SUBGRAPH_GAP=60;
|
|
317
|
+
var positions={}, curY=0;
|
|
318
|
+
components.forEach(function(comp){
|
|
319
|
+
var compSet={};comp.forEach(function(id){compSet[id]=true;});
|
|
320
|
+
// Build per-component layer groups
|
|
321
|
+
var cLg={}, cMaxLayer=0;
|
|
322
|
+
comp.forEach(function(id){var l=layers[id];if(l>cMaxLayer)cMaxLayer=l;if(!cLg[l])cLg[l]=[];cLg[l].push(nodeMap[id]);});
|
|
323
|
+
// Barycenter within component
|
|
324
|
+
for(var it=0;it<5;it++){
|
|
325
|
+
for(var l=1;l<=cMaxLayer;l++){if(!cLg[l])continue;cLg[l].forEach(function(n){var ps=parentsOf[n.id].filter(function(p){return compSet[p];});if(!ps.length){n._b=0;return;}var s=0;ps.forEach(function(p){var pg=cLg[layers[p]];s+=pg?pg.indexOf(nodeMap[p]):0;});n._b=s/ps.length;});cLg[l].sort(function(a,b){return a._b-b._b;});}
|
|
326
|
+
for(var l=cMaxLayer-1;l>=0;l--){if(!cLg[l])continue;cLg[l].forEach(function(n){var cs=childrenOf[n.id].filter(function(c){return compSet[c];});if(!cs.length){n._b=0;return;}var s=0;cs.forEach(function(c){var cg=cLg[layers[c]];s+=cg?cg.indexOf(nodeMap[c]):0;});n._b=s/cs.length;});cLg[l].sort(function(a,b){return a._b-b._b;});}
|
|
327
|
+
}
|
|
328
|
+
// Assign positions within this component, vertically centered per layer
|
|
329
|
+
var compMaxH=0;
|
|
330
|
+
for(var l=0;l<=cMaxLayer;l++){
|
|
331
|
+
var grp=cLg[l]||[],totalH=grp.length*NODE_H+(grp.length-1)*NODE_GAP;
|
|
332
|
+
if(totalH>compMaxH)compMaxH=totalH;
|
|
333
|
+
}
|
|
334
|
+
for(var l=0;l<=cMaxLayer;l++){
|
|
335
|
+
var grp=cLg[l]||[],totalH=grp.length*NODE_H+(grp.length-1)*NODE_GAP;
|
|
336
|
+
var offsetY=curY+(compMaxH-totalH)/2;
|
|
337
|
+
grp.forEach(function(n,i){positions[n.id]={x:l*LAYER_GAP,y:offsetY+i*(NODE_H+NODE_GAP)};});
|
|
338
|
+
}
|
|
339
|
+
curY+=compMaxH+SUBGRAPH_GAP;
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
// Helpers
|
|
343
|
+
function getColor(n){if(n.type==='kafka')return'var(--purple)';if(n.type==='volume')return'var(--teal)';if(n.dml_cru>1000)return'var(--red)';if(n.dml_cru>100)return'var(--orange)';if(n.dml_cru>0)return'var(--green)';return'var(--gray)';}
|
|
344
|
+
function fmt(v){return v>=1000?(v/1000).toFixed(1)+'k':v>=1?v.toFixed(1):v>0?v.toFixed(3):'0';}
|
|
345
|
+
function getBadge(n){if(n.type==='kafka')return'<span class="node-badge" style="background:rgba(168,85,247,0.15);color:var(--purple)">Kafka</span>';if(n.type==='volume')return'<span class="node-badge" style="background:rgba(20,184,166,0.15);color:var(--teal)">Vol</span>';return'';}
|
|
346
|
+
|
|
347
|
+
// Edges
|
|
348
|
+
var edgesGroup=document.createElementNS('http://www.w3.org/2000/svg','g');
|
|
349
|
+
edgesGroup.classList.add('edges-group');
|
|
350
|
+
edgesSvg.appendChild(edgesGroup);
|
|
351
|
+
var edgeEls={};
|
|
352
|
+
LINKS.forEach(function(l,i){
|
|
353
|
+
var path=document.createElementNS('http://www.w3.org/2000/svg','path');
|
|
354
|
+
path.classList.add(l.bidirectional?'edge-bidirectional':'edge-normal');
|
|
355
|
+
path.setAttribute('marker-end','url(#'+(l.bidirectional?'arrow-bi':'arrow')+')');
|
|
356
|
+
edgesGroup.appendChild(path);edgeEls[i]=path;
|
|
357
|
+
});
|
|
358
|
+
function updateEdgePaths(){
|
|
359
|
+
LINKS.forEach(function(l,i){
|
|
360
|
+
var sp=positions[l.source],tp=positions[l.target];if(!sp||!tp)return;
|
|
361
|
+
var x1=sp.x+NODE_W,y1=sp.y+NODE_H/2,x2=tp.x,y2=tp.y+NODE_H/2,dx=(x2-x1)*0.4;
|
|
362
|
+
edgeEls[i].setAttribute('d','M'+x1+','+y1+' C'+(x1+dx)+','+y1+' '+(x2-dx)+','+y2+' '+x2+','+y2);
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Nodes
|
|
367
|
+
var nodeEls={};
|
|
368
|
+
NODES.forEach(function(n){
|
|
369
|
+
var div=document.createElement('div');div.className='node-card';div.dataset.id=n.id;
|
|
370
|
+
var color=getColor(n);
|
|
371
|
+
div.innerHTML='<div class="node-header"><span class="node-type-dot" style="background:'+color+'"></span><span class="node-title" title="'+n.name+'">'+n.shortName+'</span>'+getBadge(n)+'</div><div class="node-body"><div class="node-metric"><span class="node-metric-label">DML CRU/day</span><span class="node-metric-value">'+fmt(n.dml_cru)+'</span></div><div class="node-metric"><span class="node-metric-label">Acc. CRU/day</span><span class="node-metric-value acc">'+fmt(n.accumulated_cost)+'</span></div></div><span class="port port-in"></span><span class="port port-out"></span>';
|
|
372
|
+
div.style.position='absolute';div.style.left=positions[n.id].x+'px';div.style.top=positions[n.id].y+'px';div.style.width=NODE_W+'px';
|
|
373
|
+
container.appendChild(div);nodeEls[n.id]=div;
|
|
374
|
+
});
|
|
375
|
+
updateEdgePaths();
|
|
376
|
+
|
|
377
|
+
// Store globals for interactions
|
|
378
|
+
G={NODES:NODES,LINKS:LINKS,nodeMap:nodeMap,parentsOf:parentsOf,childrenOf:childrenOf,layers:layers,maxLayer:maxLayer,positions:positions,nodeEls:nodeEls,edgeEls:edgeEls,container:container,edgesSvg:edgesSvg,canvas:canvas};
|
|
379
|
+
|
|
380
|
+
// Stats
|
|
381
|
+
var biCount=LINKS.filter(function(l){return l.bidirectional;}).length;
|
|
382
|
+
var biPairs=Math.floor(biCount/2);
|
|
383
|
+
var html='<strong>'+NODES.length+'</strong> tables, <strong>'+LINKS.length+'</strong> deps, <strong>'+(maxLayer+1)+'</strong> layers, <strong>'+components.length+'</strong> subgraphs';
|
|
384
|
+
if(biPairs>0)html+=' | <span class="warn">'+biPairs+' bidirectional</span>';
|
|
385
|
+
document.getElementById('stats').innerHTML=html;
|
|
386
|
+
|
|
387
|
+
initInteractions();
|
|
388
|
+
setTimeout(zoomFit,50);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// --- Interactions (called after render) ---
|
|
392
|
+
var vp={x:0,y:0,z:1}, panStart=null, interactionsInit=false;
|
|
393
|
+
|
|
394
|
+
function computeBounds(){
|
|
395
|
+
var minX=Infinity,minY=Infinity,maxX=-Infinity,maxY=-Infinity;
|
|
396
|
+
G.NODES.forEach(function(n){var p=G.positions[n.id];if(p.x<minX)minX=p.x;if(p.y<minY)minY=p.y;if(p.x+NODE_W>maxX)maxX=p.x+NODE_W;if(p.y+NODE_H>maxY)maxY=p.y+NODE_H;});
|
|
397
|
+
return{minX:minX,minY:minY,maxX:maxX,maxY:maxY,w:maxX-minX,h:maxY-minY};
|
|
398
|
+
}
|
|
399
|
+
function applyTransform(){
|
|
400
|
+
G.container.style.transform='translate('+vp.x+'px,'+vp.y+'px) scale('+vp.z+')';
|
|
401
|
+
G.container.style.transformOrigin='0 0';
|
|
402
|
+
var g=G.edgesSvg.querySelector('g.edges-group');
|
|
403
|
+
if(g)g.setAttribute('transform','translate('+vp.x+','+vp.y+') scale('+vp.z+')');
|
|
404
|
+
updateMinimap();
|
|
405
|
+
}
|
|
406
|
+
window.zoomFit=function(){
|
|
407
|
+
var b=computeBounds(),cw=G.canvas.clientWidth,ch=G.canvas.clientHeight,pad=80;
|
|
408
|
+
var zx=(cw-pad*2)/b.w,zy=(ch-pad*2)/b.h;
|
|
409
|
+
vp.z=Math.max(Math.min(zx,zy,1.5),0.05);
|
|
410
|
+
vp.x=(cw-b.w*vp.z)/2-b.minX*vp.z;vp.y=(ch-b.h*vp.z)/2-b.minY*vp.z;
|
|
411
|
+
applyTransform();
|
|
412
|
+
};
|
|
413
|
+
window.zoomIn=function(){vp.z=Math.min(vp.z*1.3,5);applyTransform();};
|
|
414
|
+
window.zoomOut=function(){vp.z=Math.max(vp.z*0.7,0.02);applyTransform();};
|
|
415
|
+
|
|
416
|
+
// Upstream highlight
|
|
417
|
+
var selectedId=null;
|
|
418
|
+
function getUpstream(id,vis){if(!vis)vis={};if(vis[id])return[];vis[id]=true;var r=[id];(G.parentsOf[id]||[]).forEach(function(p){r=r.concat(getUpstream(p,vis));});return r;}
|
|
419
|
+
function getDownstream(id,vis){if(!vis)vis={};if(vis[id])return[];vis[id]=true;var r=[id];(G.childrenOf[id]||[]).forEach(function(c){r=r.concat(getDownstream(c,vis));});return r;}
|
|
420
|
+
function highlightLineage(id){
|
|
421
|
+
if(selectedId===id){clearHighlight();return;}
|
|
422
|
+
selectedId=id;
|
|
423
|
+
var ups={},downs={};
|
|
424
|
+
getUpstream(id).forEach(function(u){ups[u]=true;});
|
|
425
|
+
getDownstream(id).forEach(function(d){downs[d]=true;});
|
|
426
|
+
var upE={},downE={};
|
|
427
|
+
G.LINKS.forEach(function(l,i){
|
|
428
|
+
if(ups[l.source]&&ups[l.target])upE[i]=true;
|
|
429
|
+
else if(downs[l.source]&&downs[l.target])downE[i]=true;
|
|
430
|
+
});
|
|
431
|
+
G.NODES.forEach(function(n){var el=G.nodeEls[n.id];el.classList.remove('highlighted','highlighted-down','dimmed','selected');if(n.id===id)el.classList.add('selected');else if(ups[n.id])el.classList.add('highlighted');else if(downs[n.id])el.classList.add('highlighted-down');else el.classList.add('dimmed');});
|
|
432
|
+
G.LINKS.forEach(function(l,i){var p=G.edgeEls[i];p.classList.remove('edge-highlighted','edge-highlighted-down','edge-dimmed');if(upE[i]){p.classList.add('edge-highlighted');p.setAttribute('marker-end','url(#arrow-hl)');}else if(downE[i]){p.classList.add('edge-highlighted-down');p.setAttribute('marker-end','url(#arrow-down)');}else p.classList.add('edge-dimmed');});
|
|
433
|
+
}
|
|
434
|
+
window.clearHighlight=function(){
|
|
435
|
+
selectedId=null;
|
|
436
|
+
if(!G.NODES)return;
|
|
437
|
+
G.NODES.forEach(function(n){G.nodeEls[n.id].classList.remove('highlighted','highlighted-down','dimmed','selected','search-match');});
|
|
438
|
+
G.LINKS.forEach(function(l,i){var p=G.edgeEls[i];p.classList.remove('edge-highlighted','edge-highlighted-down','edge-dimmed');p.setAttribute('marker-end','url(#'+(l.bidirectional?'arrow-bi':'arrow')+')');});
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
// Minimap
|
|
442
|
+
var mmW=180,mmH=120;
|
|
443
|
+
function updateMinimap(){
|
|
444
|
+
var mc=document.getElementById('minimap-canvas'),ctx=mc.getContext('2d'),mvp=document.getElementById('minimap-vp');
|
|
445
|
+
mc.width=mmW;mc.height=mmH;
|
|
446
|
+
if(!G.NODES||!G.NODES.length)return;
|
|
447
|
+
var b=computeBounds();if(b.w===0||b.h===0)return;
|
|
448
|
+
var pad=8,sx=(mmW-pad*2)/b.w,sy=(mmH-pad*2)/b.h,s=Math.min(sx,sy);
|
|
449
|
+
ctx.clearRect(0,0,mmW,mmH);
|
|
450
|
+
ctx.strokeStyle='rgba(100,120,160,0.3)';ctx.lineWidth=0.5;
|
|
451
|
+
G.LINKS.forEach(function(l){var sp=G.positions[l.source],tp=G.positions[l.target];if(!sp||!tp)return;ctx.beginPath();ctx.moveTo(pad+(sp.x+NODE_W/2-b.minX)*s,pad+(sp.y+NODE_H/2-b.minY)*s);ctx.lineTo(pad+(tp.x+NODE_W/2-b.minX)*s,pad+(tp.y+NODE_H/2-b.minY)*s);ctx.stroke();});
|
|
452
|
+
G.NODES.forEach(function(n){var p=G.positions[n.id],x=pad+(p.x-b.minX)*s,y=pad+(p.y-b.minY)*s;ctx.fillStyle=G.nodeEls[n.id].classList.contains('highlighted')?'#f59e0b':G.nodeEls[n.id].classList.contains('dimmed')?'rgba(100,120,160,0.2)':'#6366f1';ctx.fillRect(x,y,Math.max(NODE_W*s,2),Math.max(NODE_H*s,1));});
|
|
453
|
+
var cw=G.canvas.clientWidth,ch=G.canvas.clientHeight;
|
|
454
|
+
var vl=(-vp.x/vp.z-b.minX)*s+pad,vt=(-vp.y/vp.z-b.minY)*s+pad,vw=(cw/vp.z)*s,vh=(ch/vp.z)*s;
|
|
455
|
+
mvp.style.left=Math.max(0,vl)+'px';mvp.style.top=Math.max(0,vt)+'px';mvp.style.width=Math.min(vw,mmW)+'px';mvp.style.height=Math.min(vh,mmH)+'px';
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function initInteractions(){
|
|
459
|
+
if(interactionsInit)return;interactionsInit=true;
|
|
460
|
+
var canvas=document.getElementById('canvas');
|
|
461
|
+
// Pan
|
|
462
|
+
canvas.addEventListener('pointerdown',function(e){if(e.target.closest('.node-card')||e.target.closest('#minimap'))return;panStart={mx:e.clientX,my:e.clientY,vx:vp.x,vy:vp.y};canvas.setPointerCapture(e.pointerId);});
|
|
463
|
+
canvas.addEventListener('pointermove',function(e){if(panStart){vp.x=panStart.vx+(e.clientX-panStart.mx);vp.y=panStart.vy+(e.clientY-panStart.my);applyTransform();}});
|
|
464
|
+
canvas.addEventListener('pointerup',function(){panStart=null;});
|
|
465
|
+
// Wheel zoom
|
|
466
|
+
canvas.addEventListener('wheel',function(e){e.preventDefault();var rect=canvas.getBoundingClientRect(),mx=e.clientX-rect.left,my=e.clientY-rect.top,f=e.deltaY<0?1.1:0.9,nz=Math.max(0.02,Math.min(5,vp.z*f));vp.x=mx-(mx-vp.x)*(nz/vp.z);vp.y=my-(my-vp.y)*(nz/vp.z);vp.z=nz;applyTransform();},{passive:false});
|
|
467
|
+
// Node click
|
|
468
|
+
G.container.addEventListener('click',function(e){var c=e.target.closest('.node-card');if(c)highlightLineage(c.dataset.id);});
|
|
469
|
+
canvas.addEventListener('click',function(e){if(!e.target.closest('.node-card')&&!e.target.closest('#minimap'))clearHighlight();});
|
|
470
|
+
// Tooltip
|
|
471
|
+
var tooltip=document.getElementById('tooltip'),mouseX=0,mouseY=0;
|
|
472
|
+
document.addEventListener('mousemove',function(e){mouseX=e.clientX;mouseY=e.clientY;});
|
|
473
|
+
function posT(){if(tooltip.style.display!=='block')return;var tw=tooltip.offsetWidth,th=tooltip.offsetHeight,x=mouseX+16,y=mouseY-10;if(x+tw>window.innerWidth-10)x=mouseX-tw-16;if(y+th>window.innerHeight-10)y=window.innerHeight-th-10;tooltip.style.left=x+'px';tooltip.style.top=y+'px';}
|
|
474
|
+
G.container.addEventListener('mouseover',function(e){
|
|
475
|
+
var card=e.target.closest('.node-card');if(!card)return;var n=G.nodeMap[card.dataset.id];if(!n)return;
|
|
476
|
+
var tl='';if(n.type==='kafka')tl='<div class="tt-type" style="color:var(--purple)">Kafka Topic</div>';if(n.type==='volume')tl='<div class="tt-type" style="color:var(--teal)">Volume File</div>';
|
|
477
|
+
tooltip.innerHTML='<div class="tt-name">'+n.name+'</div>'+tl+'<div class="tt-row"><span class="tt-label">Layer</span><span class="tt-value">'+G.layers[n.id]+'</span></div><div class="tt-row"><span class="tt-label">DML CRU/day</span><span class="tt-value">'+n.dml_cru.toFixed(3)+'</span></div><div class="tt-row"><span class="tt-label">DML Jobs/day</span><span class="tt-value">'+n.dml_job_cnt+'</span></div><div class="tt-sep"></div><div class="tt-row"><span class="tt-label tt-acc">Acc. CRU/day</span><span class="tt-value tt-acc">'+n.accumulated_cost.toFixed(3)+'</span></div><div style="font-size:10px;color:var(--text2)">own + all upstream daily costs</div><div class="tt-row" style="margin-top:4px"><span class="tt-label">Query CRU/day</span><span class="tt-value">'+n.query_cru.toFixed(3)+'</span></div><div class="tt-row"><span class="tt-label">Query Jobs/day</span><span class="tt-value">'+n.query_job_cnt+'</span></div>';
|
|
478
|
+
tooltip.style.display='block';posT();
|
|
479
|
+
});
|
|
480
|
+
G.container.addEventListener('mouseout',function(e){var c=e.target.closest('.node-card');if(c&&!c.contains(e.relatedTarget))tooltip.style.display='none';});
|
|
481
|
+
setInterval(posT,50);
|
|
482
|
+
// Search
|
|
483
|
+
var si=document.getElementById('search-input'),sc=document.getElementById('search-count');
|
|
484
|
+
si.addEventListener('input',function(){
|
|
485
|
+
var q=si.value.toLowerCase().trim();
|
|
486
|
+
if(!q){clearHighlight();sc.textContent='';return;}
|
|
487
|
+
var m=0;G.NODES.forEach(function(n){var el=G.nodeEls[n.id];el.classList.remove('search-match','dimmed','highlighted','selected');if(n.name.toLowerCase().indexOf(q)>=0||n.shortName.toLowerCase().indexOf(q)>=0){el.classList.add('search-match');m++;}});
|
|
488
|
+
sc.textContent=m+' found';
|
|
489
|
+
});
|
|
490
|
+
// Minimap nav
|
|
491
|
+
var mmDrag=null,mm=document.getElementById('minimap');
|
|
492
|
+
function mmNav(mx,my){var b=computeBounds();if(b.w===0||b.h===0)return;var pad=8,sx=(mmW-pad*2)/b.w,sy=(mmH-pad*2)/b.h,s=Math.min(sx,sy),cw=G.canvas.clientWidth,ch=G.canvas.clientHeight;var gx=(mx-pad)/s+b.minX,gy=(my-pad)/s+b.minY;vp.x=-gx*vp.z+cw/2;vp.y=-gy*vp.z+ch/2;applyTransform();}
|
|
493
|
+
mm.addEventListener('pointerdown',function(e){e.preventDefault();e.stopPropagation();mm.setPointerCapture(e.pointerId);mmDrag=true;var r=mm.getBoundingClientRect();mmNav(e.clientX-r.left,e.clientY-r.top);});
|
|
494
|
+
mm.addEventListener('pointermove',function(e){if(!mmDrag)return;var r=mm.getBoundingClientRect();mmNav(e.clientX-r.left,e.clientY-r.top);});
|
|
495
|
+
mm.addEventListener('pointerup',function(){mmDrag=null;});
|
|
496
|
+
// Keyboard
|
|
497
|
+
document.addEventListener('keydown',function(e){if(e.target===si)return;if(e.key==='f'||e.key==='F')zoomFit();if(e.key==='Escape'){clearHighlight();si.value='';sc.textContent='';}if(e.key==='/'||(e.key==='k'&&(e.metaKey||e.ctrlKey))){e.preventDefault();si.focus();}});
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// --- File loading ---
|
|
501
|
+
window.loadFiles=function(){
|
|
502
|
+
var relFile=document.getElementById('file-relation').files[0];
|
|
503
|
+
var costFile=document.getElementById('file-cost').files[0];
|
|
504
|
+
var errEl=document.getElementById('upload-err');
|
|
505
|
+
if(!relFile){errEl.textContent='Please select the relation CSV file.';return;}
|
|
506
|
+
errEl.textContent='';
|
|
507
|
+
var relText='',costText='';
|
|
508
|
+
var reader1=new FileReader();
|
|
509
|
+
reader1.onload=function(e){
|
|
510
|
+
relText=e.target.result;
|
|
511
|
+
if(costFile){
|
|
512
|
+
var reader2=new FileReader();
|
|
513
|
+
reader2.onload=function(e2){costText=e2.target.result;go();};
|
|
514
|
+
reader2.readAsText(costFile);
|
|
515
|
+
}else go();
|
|
516
|
+
};
|
|
517
|
+
reader1.readAsText(relFile);
|
|
518
|
+
function go(){
|
|
519
|
+
try{
|
|
520
|
+
var relRows=parseCSV(relText);
|
|
521
|
+
if(!relRows.length){errEl.textContent='Relation CSV is empty or invalid.';return;}
|
|
522
|
+
if(!relRows[0].table_name&&!relRows[0].upstream){errEl.textContent='Relation CSV must have table_name and upstream columns.';return;}
|
|
523
|
+
var costRows=costText?parseCSV(costText):[];
|
|
524
|
+
var graph=buildGraph(relRows,costRows);
|
|
525
|
+
if(!graph.nodes.length){errEl.textContent='No tables found in the data.';return;}
|
|
526
|
+
// Show graph UI
|
|
527
|
+
document.getElementById('upload-overlay').style.display='none';
|
|
528
|
+
document.getElementById('topbar').style.display='';
|
|
529
|
+
document.getElementById('canvas').style.display='';
|
|
530
|
+
document.getElementById('legend').style.display='';
|
|
531
|
+
document.getElementById('minimap').style.display='';
|
|
532
|
+
vp={x:0,y:0,z:1};selectedId=null;
|
|
533
|
+
renderGraph(graph.nodes,graph.links);
|
|
534
|
+
}catch(ex){errEl.textContent='Error: '+ex.message;}
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
window.showUpload=function(){
|
|
538
|
+
document.getElementById('upload-overlay').style.display='';
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
// Auto-load from window.LINEAGE_DATA if available, else show file picker
|
|
542
|
+
(function(){
|
|
543
|
+
if(!window.LINEAGE_DATA)return;
|
|
544
|
+
try{
|
|
545
|
+
var d=window.LINEAGE_DATA;
|
|
546
|
+
var relRows=typeof d.relation==='string'?parseCSV(d.relation):d.relation||[];
|
|
547
|
+
var costRows=typeof d.cost==='string'?parseCSV(d.cost):d.cost||[];
|
|
548
|
+
if(!relRows.length)return;
|
|
549
|
+
var graph=buildGraph(relRows,costRows);
|
|
550
|
+
if(!graph.nodes.length)return;
|
|
551
|
+
document.getElementById('upload-overlay').style.display='none';
|
|
552
|
+
document.getElementById('topbar').style.display='';
|
|
553
|
+
document.getElementById('canvas').style.display='';
|
|
554
|
+
document.getElementById('legend').style.display='';
|
|
555
|
+
document.getElementById('minimap').style.display='';
|
|
556
|
+
vp={x:0,y:0,z:1};selectedId=null;
|
|
557
|
+
renderGraph(graph.nodes,graph.links);
|
|
558
|
+
}catch(e){}
|
|
559
|
+
})();
|
|
560
|
+
</script>
|
|
561
|
+
</body>
|
|
562
|
+
</html>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
-- 根据过去 {N} 天的作业运行情况,构建作业涉及的表的产出血缘关系图
|
|
2
|
+
with raw as (
|
|
3
|
+
select split(input_objects, ',') as input, split(output_objects, ',') as output
|
|
4
|
+
from information_schema.job_history
|
|
5
|
+
where start_time>=now() - interval {N} day
|
|
6
|
+
and output_objects is not null
|
|
7
|
+
and job_type != 'COMPACTION_JOB' -- 去掉 compaction 作业,对构建血缘关系是干扰项
|
|
8
|
+
),
|
|
9
|
+
normalized as (
|
|
10
|
+
select public.__normalize_objects(input) as input,
|
|
11
|
+
public.__normalize_objects(output) as output
|
|
12
|
+
from raw
|
|
13
|
+
),
|
|
14
|
+
exploded (
|
|
15
|
+
select table_name, explode(input) as upstream
|
|
16
|
+
from (
|
|
17
|
+
select explode(output) as table_name, input
|
|
18
|
+
from normalized
|
|
19
|
+
)
|
|
20
|
+
)
|
|
21
|
+
select table_name, upstream
|
|
22
|
+
from exploded
|
|
23
|
+
where table_name is not null and table_name != '' and upstream is not null and upstream != ''
|
|
24
|
+
group by table_name, upstream
|
|
25
|
+
;
|