This commit is contained in:
chenwu 2025-03-19 22:36:40 +08:00
parent 98b5b9855e
commit f65ab820c1
8 changed files with 307 additions and 19 deletions

View File

@ -1,5 +1,5 @@
from sys import path from sys import path
path.append('D:\\Personal_Files\\document\\GitHub\\PBI-MEASURE-CALLGRAPH\\MSNET') path.append('C:\\Users\\ChenwuServise\\OneDrive - AZCollaboration\\Desktop\\GIT\\PBI-Measure-CallGraph\\MSNET')
import pyadomd import pyadomd
import pandas as pd import pandas as pd
@ -18,8 +18,10 @@ class ModelConnector:
""" """
self.server = server self.server = server
self.database = database self.database = database
# self.connection_string = f"Provider=MSOLAP;Data Source={server}" if database == 'null' :
self.connection_string = f"Provider=MSOLAP;Data Source=localhost:56195" self.connection_string = f"Provider=MSOLAP;Data Source={server}"
else :
self.connection_string = f"Provider=MSOLAP;Data Source={server};Initial Catalog={database}"
self.connection = None self.connection = None
self.connect() self.connect()

View File

@ -37,7 +37,8 @@ h2 {
/* Main content layout - new */ /* Main content layout - new */
.main-content { .main-content {
display: flex; display: flex;
gap: 10px; gap: 0;
position: relative;
} }
.visualization-container { .visualization-container {
@ -47,6 +48,9 @@ h2 {
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
margin-bottom: 20px; margin-bottom: 20px;
flex: 4; flex: 4;
min-width: 60%;
max-width: 80%;
overflow: auto;
} }
.measure-details { .measure-details {
@ -55,13 +59,36 @@ h2 {
border-radius: 5px; border-radius: 5px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
flex: 1; flex: 1;
min-width: 30%; min-width: 20%;
max-width: 40%; max-width: 40%;
height: fit-content; height: fit-content;
position: sticky; position: sticky;
top: 20px; top: 20px;
} }
.resize-handle {
width: 8px;
background-color: #f0f0f0;
cursor: ew-resize;
transition: background-color 0.2s;
position: relative;
z-index: 10;
}
.resize-handle:hover {
background-color: #0078d4;
}
.resize-handle::after {
content: '';
position: absolute;
top: 0;
bottom: 0;
left: -4px;
right: -4px;
cursor: ew-resize;
}
/* Connection panel */ /* Connection panel */
.connection-panel { .connection-panel {
background-color: white; background-color: white;
@ -119,13 +146,60 @@ button:hover {
.controls { .controls {
display: flex; display: flex;
justify-content: space-between; flex-direction: column;
gap: 10px;
margin-bottom: 15px; margin-bottom: 15px;
} }
.search-box { .search-box {
flex-grow: 1; position: relative;
margin-left: 15px; width: 100%;
}
#reset-btn {
align-self: flex-start;
}
.search-box input {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.search-results {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: white;
border: 1px solid #ddd;
border-radius: 4px;
margin-top: 4px;
max-height: 200px;
overflow-y: auto;
z-index: 1000;
display: none;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.search-results.active {
display: block;
}
.search-result-item {
padding: 8px 12px;
cursor: pointer;
transition: background-color 0.2s;
}
.search-result-item:hover {
background-color: #f5f5f5;
}
.search-result-item.selected {
background-color: #e6f3ff;
} }
#graph-container { #graph-container {
@ -361,3 +435,49 @@ button:hover {
stroke: #d83b01; stroke: #d83b01;
stroke-width: 3px; stroke-width: 3px;
} }
.measure-controls {
margin: 10px 0;
display: flex;
gap: 10px;
}
#format-btn {
background-color: #107c10;
padding: 8px 15px;
font-size: 0.9rem;
display: flex;
align-items: center;
gap: 5px;
}
#format-btn:hover {
background-color: #0b5a0b;
}
#format-btn:active {
transform: translateY(1px);
}
.format-btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background-color: #0078d4;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.2s;
}
.format-btn:hover {
background-color: #106ebe;
}
.format-btn svg {
width: 16px;
height: 16px;
}

View File

@ -10,11 +10,13 @@ const connectionStatus = document.getElementById('connection-status');
const resetBtn = document.getElementById('reset-btn'); const resetBtn = document.getElementById('reset-btn');
const resetDetailBtn = document.getElementById('reset-detail-btn'); const resetDetailBtn = document.getElementById('reset-detail-btn');
const searchInput = document.getElementById('search-input'); const searchInput = document.getElementById('search-input');
const searchResults = document.getElementById('search-results');
const measureName = document.getElementById('measure-name'); const measureName = document.getElementById('measure-name');
const measureExpression = document.getElementById('measure-expression'); const measureExpression = document.getElementById('measure-expression');
// Graph instance // Graph instance
let graph = null; let graph = null;
let searchTimeout = null;
// Event listeners // Event listeners
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
@ -39,9 +41,12 @@ document.addEventListener('DOMContentLoaded', () => {
}); });
// Search input // Search input
searchInput.addEventListener('input', (e) => { searchInput.addEventListener('input', handleSearch);
if (graph) {
graph.searchNodes(e.target.value); // 点击其他地方时关闭搜索结果
document.addEventListener('click', (e) => {
if (!searchInput.contains(e.target) && !searchResults.contains(e.target)) {
searchResults.classList.remove('active');
} }
}); });
}); });
@ -51,11 +56,10 @@ document.addEventListener('DOMContentLoaded', () => {
*/ */
async function connectToModel() { async function connectToModel() {
const server = serverInput.value.trim() || 'localhost'; const server = serverInput.value.trim() || 'localhost';
const database = databaseInput.value.trim(); const database = databaseInput.value.trim() || 'null';
if (!database) { if (!databaseInput.value.trim()) {
showConnectionStatus('Please enter a database name', 'error'); showConnectionStatus('No database name provided, using null', 'warning');
return;
} }
try { try {
@ -123,6 +127,59 @@ function showMeasureDetails(measure) {
measureExpression.textContent = measure.expression || 'No expression available'; measureExpression.textContent = measure.expression || 'No expression available';
} }
// 处理搜索输入
function handleSearch(e) {
const query = e.target.value.trim();
// 清除之前的定时器
if (searchTimeout) {
clearTimeout(searchTimeout);
}
// 设置新的定时器,避免频繁搜索
searchTimeout = setTimeout(() => {
if (graph) {
const results = graph.searchNodes(query);
updateSearchResults(results);
}
}, 300);
}
// 更新搜索结果下拉菜单
function updateSearchResults(results) {
searchResults.innerHTML = '';
if (!results || results.length === 0) {
searchResults.classList.remove('active');
return;
}
results.forEach((node, index) => {
const div = document.createElement('div');
div.className = 'search-result-item';
div.textContent = node.id;
// 添加点击事件
div.addEventListener('click', () => {
searchInput.value = node.id;
searchResults.classList.remove('active');
graph.selectNode(node);
graph.centerViewOnNode(node);
});
// 添加键盘导航
div.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
div.click();
}
});
searchResults.appendChild(div);
});
searchResults.classList.add('active');
}
// Export functions for use in graph.js // Export functions for use in graph.js
window.appFunctions = { window.appFunctions = {
showMeasureDetails showMeasureDetails

View File

@ -658,6 +658,7 @@ class Graph {
/** /**
* Search for nodes by name * Search for nodes by name
* @param {string} query - The search query * @param {string} query - The search query
* @returns {Array} - Array of matching nodes
*/ */
searchNodes(query) { searchNodes(query) {
// 移除之前的搜索匹配标记 // 移除之前的搜索匹配标记
@ -666,20 +667,27 @@ class Graph {
if (!query) { if (!query) {
// 如果查询为空,恢复所有节点的正常显示 // 如果查询为空,恢复所有节点的正常显示
this.g.selectAll('.node').style('opacity', 1); this.g.selectAll('.node').style('opacity', 1);
return; return [];
} }
const lowerQuery = query.toLowerCase(); const lowerQuery = query.toLowerCase();
const matchingNodes = [];
// 标记匹配的节点 // 标记匹配的节点并收集匹配结果
this.g.selectAll('.node') this.g.selectAll('.node')
.each((d, i, nodes) => { .each((d, i, nodes) => {
const node = d3.select(nodes[i]); const node = d3.select(nodes[i]);
const isMatch = d.id.toLowerCase().includes(lowerQuery); const isMatch = d.id.toLowerCase().includes(lowerQuery);
if (isMatch) {
matchingNodes.push(d);
}
// 为匹配的节点添加特殊类并使其文本加粗 // 为匹配的节点添加特殊类并使其文本加粗
node.classed('search-match', isMatch); node.classed('search-match', isMatch);
}); });
return matchingNodes;
} }
/** /**

58
static/js/resize.js Normal file
View File

@ -0,0 +1,58 @@
document.addEventListener('DOMContentLoaded', function() {
const resizeHandle = document.querySelector('.resize-handle');
const visualizationContainer = document.querySelector('.visualization-container');
const measureDetails = document.querySelector('.measure-details');
const mainContent = document.querySelector('.main-content');
let isResizing = false;
let startX;
let startWidth;
let startFlex;
resizeHandle.addEventListener('mousedown', function(e) {
isResizing = true;
startX = e.pageX;
startWidth = visualizationContainer.offsetWidth;
startFlex = parseFloat(getComputedStyle(visualizationContainer).flex);
document.body.style.cursor = 'ew-resize';
document.body.style.userSelect = 'none';
});
document.addEventListener('mousemove', function(e) {
if (!isResizing) return;
const deltaX = e.pageX - startX;
const containerWidth = mainContent.offsetWidth;
const newWidth = startWidth + deltaX;
const widthPercent = (newWidth / containerWidth) * 100;
// 确保宽度在限制范围内
if (widthPercent >= 60 && widthPercent <= 80) {
visualizationContainer.style.flex = 'none';
visualizationContainer.style.width = `${widthPercent}%`;
measureDetails.style.width = `${100 - widthPercent}%`;
}
});
document.addEventListener('mouseup', function() {
isResizing = false;
document.body.style.cursor = '';
document.body.style.userSelect = '';
// 保存当前宽度比例到 localStorage
const widthPercent = (visualizationContainer.offsetWidth / mainContent.offsetWidth) * 100;
localStorage.setItem('visualizationWidth', widthPercent);
});
// 恢复保存的宽度比例
const savedWidth = localStorage.getItem('visualizationWidth');
if (savedWidth) {
const widthPercent = parseFloat(savedWidth);
if (widthPercent >= 60 && widthPercent <= 80) {
visualizationContainer.style.flex = 'none';
visualizationContainer.style.width = `${widthPercent}%`;
measureDetails.style.width = `${100 - widthPercent}%`;
}
}
});

View File

@ -7,6 +7,38 @@
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<!-- D3.js for visualization --> <!-- D3.js for visualization -->
<script src="https://d3js.org/d3.v7.min.js"></script> <script src="https://d3js.org/d3.v7.min.js"></script>
<script>
async function formatDAX() {
const measureExpression = document.getElementById('measure-expression');
const daxCode = measureExpression.textContent;
try {
const formData = new FormData();
formData.append('fx', daxCode);
formData.append('r', 'US');
const response = await fetch('https://www.daxformatter.com', {
method: 'POST',
body: formData
});
if (!response.ok) {
throw new Error('格式化请求失败');
}
const result = await response.text();
// 解析返回的HTML提取格式化后的DAX代码
const parser = new DOMParser();
const doc = parser.parseFromString(result, 'text/html');
const formattedDax = doc.querySelector('#formatted-dax').textContent;
measureExpression.textContent = formattedDax;
} catch (error) {
console.error('格式化DAX时出错:', error);
alert('格式化DAX时出错请检查网络连接或稍后重试');
}
}
</script>
</head> </head>
<body> <body>
<div class="container"> <div class="container">
@ -35,7 +67,8 @@
<div class="controls"> <div class="controls">
<button id="reset-btn">Reset View</button> <button id="reset-btn">Reset View</button>
<div class="search-box"> <div class="search-box">
<input type="text" id="search-input" placeholder="Search measures..."> <input type="text" id="search-input" placeholder="search...">
<div id="search-results" class="search-results"></div>
</div> </div>
</div> </div>
<div id="graph-container"></div> <div id="graph-container"></div>
@ -85,10 +118,19 @@
}); });
</script> </script>
</div> </div>
<div class="resize-handle"></div>
<div class="measure-details"> <div class="measure-details">
<h2>Measure Details</h2> <h2>Measure Details</h2>
<div id="measure-name"></div> <div id="measure-name"></div>
<div class="measure-controls">
<button id="format-btn" class="format-btn" onclick="formatDAX()">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path>
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path>
</svg>
格式化 DAX
</button>
</div>
<pre id="measure-expression"></pre> <pre id="measure-expression"></pre>
</div> </div>
</div> </div>
@ -96,5 +138,6 @@
<script src="{{ url_for('static', filename='js/graph.js') }}"></script> <script src="{{ url_for('static', filename='js/graph.js') }}"></script>
<script src="{{ url_for('static', filename='js/app.js') }}"></script> <script src="{{ url_for('static', filename='js/app.js') }}"></script>
<script src="{{ url_for('static', filename='js/resize.js') }}"></script>
</body> </body>
</html> </html>