diff --git a/__pycache__/dax_parser.cpython-313.pyc b/__pycache__/dax_parser.cpython-313.pyc index ee35d03..9910f5d 100644 Binary files a/__pycache__/dax_parser.cpython-313.pyc and b/__pycache__/dax_parser.cpython-313.pyc differ diff --git a/__pycache__/model_connector.cpython-313.pyc b/__pycache__/model_connector.cpython-313.pyc index 7c11d07..0fed43c 100644 Binary files a/__pycache__/model_connector.cpython-313.pyc and b/__pycache__/model_connector.cpython-313.pyc differ diff --git a/model_connector.py b/model_connector.py index 811a327..07393f5 100644 --- a/model_connector.py +++ b/model_connector.py @@ -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() diff --git a/static/css/style.css b/static/css/style.css index 3d4ed98..eed598f 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -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; +} diff --git a/static/js/app.js b/static/js/app.js index 21a1108..0fd94cf 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -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 diff --git a/static/js/graph.js b/static/js/graph.js index a8a612e..1beb999 100644 --- a/static/js/graph.js +++ b/static/js/graph.js @@ -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; } /** diff --git a/static/js/resize.js b/static/js/resize.js new file mode 100644 index 0000000..cb6cfa3 --- /dev/null +++ b/static/js/resize.js @@ -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}%`; + } + } +}); \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index 4a06070..2c5d5cd 100644 --- a/templates/index.html +++ b/templates/index.html @@ -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>