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
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 pandas as pd
@ -18,8 +18,10 @@ class ModelConnector:
"""
self.server = server
self.database = database
# self.connection_string = f"Provider=MSOLAP;Data Source={server}"
self.connection_string = f"Provider=MSOLAP;Data Source=localhost:56195"
if database == 'null' :
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.connect()

View File

@ -37,7 +37,8 @@ h2 {
/* Main content layout - new */
.main-content {
display: flex;
gap: 10px;
gap: 0;
position: relative;
}
.visualization-container {
@ -47,6 +48,9 @@ h2 {
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
flex: 4;
min-width: 60%;
max-width: 80%;
overflow: auto;
}
.measure-details {
@ -55,13 +59,36 @@ h2 {
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
flex: 1;
min-width: 30%;
min-width: 20%;
max-width: 40%;
height: fit-content;
position: sticky;
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 {
background-color: white;
@ -119,13 +146,60 @@ button:hover {
.controls {
display: flex;
justify-content: space-between;
flex-direction: column;
gap: 10px;
margin-bottom: 15px;
}
.search-box {
flex-grow: 1;
margin-left: 15px;
position: relative;
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 {
@ -361,3 +435,49 @@ button:hover {
stroke: #d83b01;
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 resetDetailBtn = document.getElementById('reset-detail-btn');
const searchInput = document.getElementById('search-input');
const searchResults = document.getElementById('search-results');
const measureName = document.getElementById('measure-name');
const measureExpression = document.getElementById('measure-expression');
// Graph instance
let graph = null;
let searchTimeout = null;
// Event listeners
document.addEventListener('DOMContentLoaded', () => {
@ -39,9 +41,12 @@ document.addEventListener('DOMContentLoaded', () => {
});
// Search input
searchInput.addEventListener('input', (e) => {
if (graph) {
graph.searchNodes(e.target.value);
searchInput.addEventListener('input', handleSearch);
// 点击其他地方时关闭搜索结果
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() {
const server = serverInput.value.trim() || 'localhost';
const database = databaseInput.value.trim();
const database = databaseInput.value.trim() || 'null';
if (!database) {
showConnectionStatus('Please enter a database name', 'error');
return;
if (!databaseInput.value.trim()) {
showConnectionStatus('No database name provided, using null', 'warning');
}
try {
@ -123,6 +127,59 @@ function showMeasureDetails(measure) {
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
window.appFunctions = {
showMeasureDetails

View File

@ -658,6 +658,7 @@ class Graph {
/**
* Search for nodes by name
* @param {string} query - The search query
* @returns {Array} - Array of matching nodes
*/
searchNodes(query) {
// 移除之前的搜索匹配标记
@ -666,20 +667,27 @@ class Graph {
if (!query) {
// 如果查询为空,恢复所有节点的正常显示
this.g.selectAll('.node').style('opacity', 1);
return;
return [];
}
const lowerQuery = query.toLowerCase();
const matchingNodes = [];
// 标记匹配的节点
// 标记匹配的节点并收集匹配结果
this.g.selectAll('.node')
.each((d, i, nodes) => {
const node = d3.select(nodes[i]);
const isMatch = d.id.toLowerCase().includes(lowerQuery);
if (isMatch) {
matchingNodes.push(d);
}
// 为匹配的节点添加特殊类并使其文本加粗
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') }}">
<!-- D3.js for visualization -->
<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>
<body>
<div class="container">
@ -35,7 +67,8 @@
<div class="controls">
<button id="reset-btn">Reset View</button>
<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 id="graph-container"></div>
@ -85,10 +118,19 @@
});
</script>
</div>
<div class="resize-handle"></div>
<div class="measure-details">
<h2>Measure Details</h2>
<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>
</div>
</div>
@ -96,5 +138,6 @@
<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/resize.js') }}"></script>
</body>
</html>