update1
This commit is contained in:
parent
98b5b9855e
commit
f65ab820c1
Binary file not shown.
Binary file not shown.
@ -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()
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
58
static/js/resize.js
Normal 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}%`;
|
||||
}
|
||||
}
|
||||
});
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user