powerbi-visuals-tools 7.0.2 → 7.1.0
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/Changelog.md +17 -0
- package/MCP.md +234 -0
- package/bin/pbiviz.js +12 -0
- package/eslint.config.mjs +12 -9
- package/lib/CertificateTools.js +1 -1
- package/lib/CommandManager.js +9 -1
- package/lib/ConsoleWriter.js +4 -0
- package/lib/FeatureManager.js +9 -3
- package/lib/Visual.js +6 -0
- package/lib/VisualGenerator.js +2 -2
- package/lib/VisualManager.js +22 -6
- package/lib/WebPackWrap.js +8 -4
- package/lib/features/AuthorInfo.js +14 -0
- package/lib/features/BaseFeature.js +1 -0
- package/lib/features/RenderingEvents.js +1 -0
- package/lib/features/index.js +2 -1
- package/lib/mcp/McpServer.js +122 -0
- package/lib/mcp/tools/availableApis.js +608 -0
- package/lib/mcp/tools/bestPractices.js +391 -0
- package/lib/mcp/tools/certification.js +380 -0
- package/lib/mcp/tools/visualInfo.js +133 -0
- package/lib/mcp/tools/vulnerabilities.js +211 -0
- package/lib/utils.js +27 -0
- package/package.json +22 -18
- package/templates/visuals/_global/.vscode/mcp.json +8 -0
- package/templates/visuals/default/src/visual.ts +17 -4
- package/templates/visuals/rhtml/src/visual.ts +27 -11
- package/templates/visuals/rvisual/src/visual.ts +34 -20
- package/templates/visuals/slicer/src/visual.ts +16 -4
- package/templates/visuals/table/src/visual.ts +14 -1
- package/tsconfig.json +2 -1
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Power BI Visual CLI - MCP Server - Vulnerabilities Check Tool
|
|
3
|
+
*
|
|
4
|
+
* Copyright (c) Microsoft Corporation
|
|
5
|
+
* All rights reserved.
|
|
6
|
+
* MIT License
|
|
7
|
+
*/
|
|
8
|
+
"use strict";
|
|
9
|
+
import fs from 'fs-extra';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import { getSourceFiles } from '../../utils.js';
|
|
12
|
+
// Dangerous patterns in code
|
|
13
|
+
const CODE_PATTERNS = [
|
|
14
|
+
{ pattern: /eval\s*\(/, message: 'Use of eval() is a security risk', severity: 'critical' },
|
|
15
|
+
{ pattern: /new\s+Function\s*\(/, message: 'Dynamic Function construction is a security risk', severity: 'critical' },
|
|
16
|
+
{ pattern: /innerHTML\s*=/, message: 'Direct innerHTML assignment may cause XSS vulnerabilities', severity: 'medium' },
|
|
17
|
+
{ pattern: /document\.write/, message: 'document.write is deprecated and can be dangerous', severity: 'medium' },
|
|
18
|
+
{ pattern: /window\.location\s*=/, message: 'Direct location assignment might be a security concern', severity: 'low' },
|
|
19
|
+
{ pattern: /\bfetch\s*\(/, message: 'Use of fetch() is not allowed in certified visuals (external network calls are prohibited)', severity: 'high' },
|
|
20
|
+
{ pattern: /XMLHttpRequest/, message: 'XMLHttpRequest to external URLs is not allowed in certified visuals', severity: 'high' },
|
|
21
|
+
];
|
|
22
|
+
async function checkSourceCode(rootPath) {
|
|
23
|
+
const results = [];
|
|
24
|
+
const srcPath = path.join(rootPath, 'src');
|
|
25
|
+
if (!fs.existsSync(srcPath)) {
|
|
26
|
+
return results;
|
|
27
|
+
}
|
|
28
|
+
const files = await getSourceFiles(srcPath);
|
|
29
|
+
for (const file of files) {
|
|
30
|
+
const content = await fs.readFile(file, 'utf-8');
|
|
31
|
+
const lines = content.split('\n');
|
|
32
|
+
const relativePath = path.relative(rootPath, file);
|
|
33
|
+
let inBlockComment = false;
|
|
34
|
+
for (let i = 0; i < lines.length; i++) {
|
|
35
|
+
const line = lines[i];
|
|
36
|
+
// Split line into activeCode and commentedCode by walking
|
|
37
|
+
// left-to-right through /*, */, and // markers
|
|
38
|
+
let activeCode = '';
|
|
39
|
+
let commentedCode = '';
|
|
40
|
+
let pos = 0;
|
|
41
|
+
while (pos < line.length) {
|
|
42
|
+
if (inBlockComment) {
|
|
43
|
+
const endIdx = line.indexOf('*/', pos);
|
|
44
|
+
if (endIdx !== -1) {
|
|
45
|
+
commentedCode += line.substring(pos, endIdx + 2);
|
|
46
|
+
pos = endIdx + 2;
|
|
47
|
+
inBlockComment = false;
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
commentedCode += line.substring(pos);
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
const blockStart = line.indexOf('/*', pos);
|
|
56
|
+
const lineComment = line.indexOf('//', pos);
|
|
57
|
+
// Find which comment marker comes first
|
|
58
|
+
let nextComment = -1;
|
|
59
|
+
let isBlock = false;
|
|
60
|
+
if (blockStart !== -1 && (lineComment === -1 || blockStart <= lineComment)) {
|
|
61
|
+
nextComment = blockStart;
|
|
62
|
+
isBlock = true;
|
|
63
|
+
}
|
|
64
|
+
else if (lineComment !== -1) {
|
|
65
|
+
nextComment = lineComment;
|
|
66
|
+
}
|
|
67
|
+
if (nextComment === -1) {
|
|
68
|
+
activeCode += line.substring(pos);
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
activeCode += line.substring(pos, nextComment);
|
|
72
|
+
if (isBlock) {
|
|
73
|
+
const endIdx = line.indexOf('*/', nextComment + 2);
|
|
74
|
+
if (endIdx !== -1) {
|
|
75
|
+
commentedCode += line.substring(nextComment, endIdx + 2);
|
|
76
|
+
pos = endIdx + 2;
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
commentedCode += line.substring(nextComment);
|
|
80
|
+
inBlockComment = true;
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
// Line comment — rest of line is commented
|
|
86
|
+
commentedCode += line.substring(nextComment);
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// Check all patterns against the appropriate segment
|
|
92
|
+
for (const codePattern of CODE_PATTERNS) {
|
|
93
|
+
const inActive = codePattern.pattern.test(activeCode);
|
|
94
|
+
const inComment = codePattern.pattern.test(commentedCode);
|
|
95
|
+
if (inActive) {
|
|
96
|
+
results.push({
|
|
97
|
+
severity: codePattern.severity,
|
|
98
|
+
message: `${relativePath}:${i + 1}: ${codePattern.message}`
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
else if (inComment) {
|
|
102
|
+
results.push({
|
|
103
|
+
severity: 'info',
|
|
104
|
+
message: `${relativePath}:${i + 1}: ${codePattern.message} (commented out)`,
|
|
105
|
+
recommendation: 'Remove commented-out dangerous code to keep the codebase clean'
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return results;
|
|
112
|
+
}
|
|
113
|
+
function formatResults(results) {
|
|
114
|
+
if (results.length === 0) {
|
|
115
|
+
return `✅ **No vulnerabilities detected!**
|
|
116
|
+
|
|
117
|
+
Your visual project passed the security scan. Keep following best practices:
|
|
118
|
+
- Regularly update dependencies
|
|
119
|
+
- Run \`npm audit\` periodically
|
|
120
|
+
- Avoid external network calls
|
|
121
|
+
- Sanitize all user inputs
|
|
122
|
+
`;
|
|
123
|
+
}
|
|
124
|
+
const grouped = {
|
|
125
|
+
critical: results.filter(r => r.severity === 'critical'),
|
|
126
|
+
high: results.filter(r => r.severity === 'high'),
|
|
127
|
+
medium: results.filter(r => r.severity === 'medium'),
|
|
128
|
+
low: results.filter(r => r.severity === 'low'),
|
|
129
|
+
info: results.filter(r => r.severity === 'info'),
|
|
130
|
+
};
|
|
131
|
+
let output = `# 🔍 Vulnerability Scan Results\n\n`;
|
|
132
|
+
output += `Found **${results.length}** issue(s)\n\n`;
|
|
133
|
+
if (grouped.critical.length > 0) {
|
|
134
|
+
output += `## 🔴 Critical (${grouped.critical.length})\n`;
|
|
135
|
+
grouped.critical.forEach(r => {
|
|
136
|
+
output += `- ${r.message}\n`;
|
|
137
|
+
if (r.recommendation)
|
|
138
|
+
output += ` 💡 ${r.recommendation}\n`;
|
|
139
|
+
});
|
|
140
|
+
output += '\n';
|
|
141
|
+
}
|
|
142
|
+
if (grouped.high.length > 0) {
|
|
143
|
+
output += `## 🟠 High (${grouped.high.length})\n`;
|
|
144
|
+
grouped.high.forEach(r => {
|
|
145
|
+
output += `- ${r.message}\n`;
|
|
146
|
+
if (r.recommendation)
|
|
147
|
+
output += ` 💡 ${r.recommendation}\n`;
|
|
148
|
+
});
|
|
149
|
+
output += '\n';
|
|
150
|
+
}
|
|
151
|
+
if (grouped.medium.length > 0) {
|
|
152
|
+
output += `## 🟡 Medium (${grouped.medium.length})\n`;
|
|
153
|
+
grouped.medium.forEach(r => {
|
|
154
|
+
output += `- ${r.message}\n`;
|
|
155
|
+
if (r.recommendation)
|
|
156
|
+
output += ` 💡 ${r.recommendation}\n`;
|
|
157
|
+
});
|
|
158
|
+
output += '\n';
|
|
159
|
+
}
|
|
160
|
+
if (grouped.low.length > 0) {
|
|
161
|
+
output += `## 🟢 Low (${grouped.low.length})\n`;
|
|
162
|
+
grouped.low.forEach(r => {
|
|
163
|
+
output += `- ${r.message}\n`;
|
|
164
|
+
if (r.recommendation)
|
|
165
|
+
output += ` 💡 ${r.recommendation}\n`;
|
|
166
|
+
});
|
|
167
|
+
output += '\n';
|
|
168
|
+
}
|
|
169
|
+
if (grouped.info.length > 0) {
|
|
170
|
+
output += `## ℹ️ Info (${grouped.info.length})\n`;
|
|
171
|
+
grouped.info.forEach(r => {
|
|
172
|
+
output += `- ${r.message}\n`;
|
|
173
|
+
if (r.recommendation)
|
|
174
|
+
output += ` 💡 ${r.recommendation}\n`;
|
|
175
|
+
});
|
|
176
|
+
output += '\n';
|
|
177
|
+
}
|
|
178
|
+
output += `---\n`;
|
|
179
|
+
output += `Run \`npm audit\` for a more comprehensive dependency audit.\n`;
|
|
180
|
+
return output;
|
|
181
|
+
}
|
|
182
|
+
async function checkEslint(rootPath) {
|
|
183
|
+
const results = [];
|
|
184
|
+
const packageJsonPath = path.join(rootPath, 'package.json');
|
|
185
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
186
|
+
return results;
|
|
187
|
+
}
|
|
188
|
+
const packageJson = await fs.readJson(packageJsonPath);
|
|
189
|
+
const allDeps = {
|
|
190
|
+
...packageJson.dependencies,
|
|
191
|
+
...packageJson.devDependencies
|
|
192
|
+
};
|
|
193
|
+
if (!allDeps['eslint']) {
|
|
194
|
+
results.push({
|
|
195
|
+
severity: 'info',
|
|
196
|
+
message: 'ESLint is not configured',
|
|
197
|
+
recommendation: 'Add ESLint for code quality and security linting'
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
return results;
|
|
201
|
+
}
|
|
202
|
+
export async function checkVulnerabilities(rootPath) {
|
|
203
|
+
try {
|
|
204
|
+
const codeResults = await checkSourceCode(rootPath);
|
|
205
|
+
const eslintResults = await checkEslint(rootPath);
|
|
206
|
+
return formatResults([...codeResults, ...eslintResults]);
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
return `❌ Error scanning project: ${(error instanceof Error) ? error.message : String(error)}`;
|
|
210
|
+
}
|
|
211
|
+
}
|
package/lib/utils.js
CHANGED
|
@@ -25,6 +25,33 @@ export function getRootPath() {
|
|
|
25
25
|
export function getJsPath(filePath) {
|
|
26
26
|
return filePath.replace(/\.json$/, '.mjs');
|
|
27
27
|
}
|
|
28
|
+
export async function getSourceFiles(dir) {
|
|
29
|
+
const files = [];
|
|
30
|
+
if (!fs.existsSync(dir))
|
|
31
|
+
return files;
|
|
32
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
33
|
+
for (const entry of entries) {
|
|
34
|
+
const fullPath = path.join(dir, entry.name);
|
|
35
|
+
if (entry.isDirectory() && entry.name !== 'node_modules' && entry.name !== '.tmp') {
|
|
36
|
+
files.push(...await getSourceFiles(fullPath));
|
|
37
|
+
}
|
|
38
|
+
else if (entry.isFile() && /\.(ts|js|tsx|jsx)$/.test(entry.name)) {
|
|
39
|
+
files.push(fullPath);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return files;
|
|
43
|
+
}
|
|
44
|
+
export function existsIgnoreCase(filePath) {
|
|
45
|
+
const dir = path.dirname(filePath);
|
|
46
|
+
const name = path.basename(filePath).toLowerCase();
|
|
47
|
+
try {
|
|
48
|
+
const entries = fs.readdirSync(dir);
|
|
49
|
+
return entries.some(entry => entry.toLowerCase() === name);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
28
55
|
async function safelyImport(filePath) {
|
|
29
56
|
return fs.existsSync(filePath) && (await import(`file://${filePath}`)).default;
|
|
30
57
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "powerbi-visuals-tools",
|
|
3
|
-
"version": "7.0
|
|
3
|
+
"version": "7.1.0",
|
|
4
4
|
"description": "Command line tool for creating and publishing visuals for Power BI",
|
|
5
5
|
"main": "./bin/pbiviz.js",
|
|
6
6
|
"type": "module",
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"jasmine": "node spec/jasmine-runner.js",
|
|
13
13
|
"jasmine-inspect": "node --inspect spec/jasmine-runner.js",
|
|
14
14
|
"lint": "npx eslint .",
|
|
15
|
+
"lint:fix": "npx eslint . --fix",
|
|
15
16
|
"debug-tests": "npm run lint && npm run jasmine-inspect"
|
|
16
17
|
},
|
|
17
18
|
"bin": {
|
|
@@ -28,39 +29,42 @@
|
|
|
28
29
|
},
|
|
29
30
|
"homepage": "https://github.com/Microsoft/PowerBI-visuals-tools#readme",
|
|
30
31
|
"dependencies": {
|
|
32
|
+
"@modelcontextprotocol/sdk": "^1.25.3",
|
|
31
33
|
"@typescript-eslint/parser": "^8.50.0",
|
|
32
34
|
"async": "^3.2.6",
|
|
33
35
|
"chalk": "^5.4.1",
|
|
34
|
-
"commander": "^
|
|
36
|
+
"commander": "^14.0.3",
|
|
35
37
|
"compare-versions": "^6.1.1",
|
|
36
|
-
"css-loader": "^7.1.
|
|
37
|
-
"eslint-plugin-powerbi-visuals": "1.1.
|
|
38
|
-
"extra
|
|
39
|
-
"fs-extra": "^11.3.3",
|
|
38
|
+
"css-loader": "^7.1.4",
|
|
39
|
+
"eslint-plugin-powerbi-visuals": "1.1.1",
|
|
40
|
+
"fs-extra": "^11.3.4",
|
|
40
41
|
"inline-source-map": "^0.6.3",
|
|
41
42
|
"json-loader": "0.5.7",
|
|
42
43
|
"jszip": "^3.10.1",
|
|
43
|
-
"less": "^4.
|
|
44
|
-
"less-loader": "^12.3.
|
|
44
|
+
"less": "^4.6.4",
|
|
45
|
+
"less-loader": "^12.3.2",
|
|
45
46
|
"lodash.clonedeep": "4.5.0",
|
|
46
47
|
"lodash.defaults": "4.2.0",
|
|
47
48
|
"lodash.isequal": "4.5.0",
|
|
48
49
|
"lodash.ismatch": "^4.4.0",
|
|
49
|
-
"mini-css-extract-plugin": "^2.
|
|
50
|
-
"powerbi-visuals-webpack-plugin": "^
|
|
51
|
-
"terser-webpack-plugin": "^5.
|
|
50
|
+
"mini-css-extract-plugin": "^2.10.2",
|
|
51
|
+
"powerbi-visuals-webpack-plugin": "^5.0.1",
|
|
52
|
+
"terser-webpack-plugin": "^5.4.0",
|
|
52
53
|
"ts-loader": "^9.5.4",
|
|
53
54
|
"typescript": "^5.9.3",
|
|
54
|
-
"webpack": "^5.
|
|
55
|
+
"webpack": "^5.105.4",
|
|
55
56
|
"webpack-bundle-analyzer": "4.10.2",
|
|
56
|
-
"webpack-dev-server": "^5.2.
|
|
57
|
+
"webpack-dev-server": "^5.2.3",
|
|
58
|
+
"zod": "^4.3.6"
|
|
59
|
+
|
|
57
60
|
},
|
|
58
61
|
"devDependencies": {
|
|
59
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
60
|
-
"eslint": "^
|
|
61
|
-
"
|
|
62
|
+
"@typescript-eslint/eslint-plugin": "^8.58.0",
|
|
63
|
+
"eslint": "^10.1.0",
|
|
64
|
+
"globals": "^17.4.0",
|
|
65
|
+
"jasmine": "^6.1.0",
|
|
62
66
|
"jasmine-spec-reporter": "7.0.0",
|
|
63
|
-
"semver": "7.7.
|
|
67
|
+
"semver": "7.7.4",
|
|
64
68
|
"tree-kill": "1.2.2",
|
|
65
69
|
"webpack-cli": "^6.0.1"
|
|
66
70
|
},
|
|
@@ -68,6 +72,6 @@
|
|
|
68
72
|
"fsevents": "*"
|
|
69
73
|
},
|
|
70
74
|
"engines": {
|
|
71
|
-
"node": ">=
|
|
75
|
+
"node": ">=20.19.0"
|
|
72
76
|
}
|
|
73
77
|
}
|
|
@@ -32,10 +32,12 @@ import "./../style/visual.less";
|
|
|
32
32
|
import VisualConstructorOptions = powerbi.extensibility.visual.VisualConstructorOptions;
|
|
33
33
|
import VisualUpdateOptions = powerbi.extensibility.visual.VisualUpdateOptions;
|
|
34
34
|
import IVisual = powerbi.extensibility.visual.IVisual;
|
|
35
|
+
import IVisualEventService = powerbi.extensibility.IVisualEventService;
|
|
35
36
|
|
|
36
37
|
import { VisualFormattingSettingsModel } from "./settings";
|
|
37
38
|
|
|
38
39
|
export class Visual implements IVisual {
|
|
40
|
+
private events: IVisualEventService;
|
|
39
41
|
private target: HTMLElement;
|
|
40
42
|
private updateCount: number;
|
|
41
43
|
private textNode: Text;
|
|
@@ -44,6 +46,7 @@ export class Visual implements IVisual {
|
|
|
44
46
|
|
|
45
47
|
constructor(options: VisualConstructorOptions) {
|
|
46
48
|
console.log('Visual constructor', options);
|
|
49
|
+
this.events = options.host.eventService;
|
|
47
50
|
this.formattingSettingsService = new FormattingSettingsService();
|
|
48
51
|
this.target = options.element;
|
|
49
52
|
this.updateCount = 0;
|
|
@@ -59,11 +62,21 @@ export class Visual implements IVisual {
|
|
|
59
62
|
}
|
|
60
63
|
|
|
61
64
|
public update(options: VisualUpdateOptions) {
|
|
62
|
-
this.
|
|
65
|
+
this.events.renderingStarted(options);
|
|
63
66
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
try {
|
|
68
|
+
this.formattingSettings = this.formattingSettingsService.populateFormattingSettingsModel(VisualFormattingSettingsModel, options.dataViews[0]);
|
|
69
|
+
|
|
70
|
+
console.log('Visual update', options);
|
|
71
|
+
if (this.textNode) {
|
|
72
|
+
this.textNode.textContent = (this.updateCount++).toString();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
this.events.renderingFinished(options);
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
console.log('Error in update method', error);
|
|
79
|
+
this.events.renderingFailed(options, String(error))
|
|
67
80
|
}
|
|
68
81
|
}
|
|
69
82
|
|
|
@@ -30,6 +30,8 @@ import { formattingSettings, FormattingSettingsService } from "powerbi-visuals-u
|
|
|
30
30
|
import VisualConstructorOptions = powerbi.extensibility.visual.VisualConstructorOptions;
|
|
31
31
|
import VisualUpdateOptions = powerbi.extensibility.visual.VisualUpdateOptions;
|
|
32
32
|
import IVisual = powerbi.extensibility.visual.IVisual;
|
|
33
|
+
import IVisualEventService = powerbi.extensibility.IVisualEventService;
|
|
34
|
+
|
|
33
35
|
import DataView = powerbi.DataView;
|
|
34
36
|
import IViewport = powerbi.IViewport;
|
|
35
37
|
|
|
@@ -85,6 +87,7 @@ const renderVisualUpdateType: number[] = [
|
|
|
85
87
|
];
|
|
86
88
|
|
|
87
89
|
export class Visual implements IVisual {
|
|
90
|
+
private events: IVisualEventService;
|
|
88
91
|
private rootElement: HTMLElement;
|
|
89
92
|
private headNodes: Node[];
|
|
90
93
|
private bodyNodes: Node[];
|
|
@@ -92,6 +95,8 @@ export class Visual implements IVisual {
|
|
|
92
95
|
private formattingSettingsService: FormattingSettingsService;
|
|
93
96
|
|
|
94
97
|
public constructor(options: VisualConstructorOptions) {
|
|
98
|
+
this.events = options.host.eventService;
|
|
99
|
+
|
|
95
100
|
this.formattingSettingsService = new FormattingSettingsService();
|
|
96
101
|
if (options && options.element) {
|
|
97
102
|
this.rootElement = options.element;
|
|
@@ -101,29 +106,40 @@ export class Visual implements IVisual {
|
|
|
101
106
|
}
|
|
102
107
|
|
|
103
108
|
public update(options: VisualUpdateOptions): void {
|
|
109
|
+
this.events.renderingStarted(options);
|
|
110
|
+
|
|
104
111
|
if (!options ||
|
|
105
112
|
!options.type ||
|
|
106
113
|
!options.viewport ||
|
|
107
114
|
!options.dataViews ||
|
|
108
115
|
options.dataViews.length === 0 ||
|
|
109
116
|
!options.dataViews[0]) {
|
|
117
|
+
this.events.renderingFinished(options);
|
|
110
118
|
return;
|
|
111
119
|
}
|
|
112
120
|
|
|
113
|
-
|
|
114
|
-
|
|
121
|
+
try {
|
|
122
|
+
const dataView: DataView = options.dataViews[0];
|
|
123
|
+
this.formattingSettings = this.formattingSettingsService.populateFormattingSettingsModel(VisualFormattingSettingsModel, options.dataViews[0]);
|
|
115
124
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
125
|
+
let payloadBase64: string = null;
|
|
126
|
+
if (dataView.scriptResult && dataView.scriptResult.payloadBase64) {
|
|
127
|
+
payloadBase64 = dataView.scriptResult.payloadBase64;
|
|
128
|
+
}
|
|
120
129
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
130
|
+
if (renderVisualUpdateType.indexOf(options.type) === -1) {
|
|
131
|
+
if (payloadBase64) {
|
|
132
|
+
this.injectCodeFromPayload(payloadBase64);
|
|
133
|
+
}
|
|
134
|
+
} else {
|
|
135
|
+
this.onResizing(options.viewport);
|
|
124
136
|
}
|
|
125
|
-
|
|
126
|
-
this.
|
|
137
|
+
|
|
138
|
+
this.events.renderingFinished(options);
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
console.log('Error in update method', error);
|
|
142
|
+
this.events.renderingFailed(options, String(error))
|
|
127
143
|
}
|
|
128
144
|
}
|
|
129
145
|
|
|
@@ -31,18 +31,22 @@ import { FormattingSettingsService } from "powerbi-visuals-utils-formattingmodel
|
|
|
31
31
|
import VisualConstructorOptions = powerbi.extensibility.visual.VisualConstructorOptions;
|
|
32
32
|
import VisualUpdateOptions = powerbi.extensibility.visual.VisualUpdateOptions;
|
|
33
33
|
import IVisual = powerbi.extensibility.visual.IVisual;
|
|
34
|
+
import IVisualEventService = powerbi.extensibility.IVisualEventService;
|
|
34
35
|
import DataView = powerbi.DataView;
|
|
35
36
|
import IViewport = powerbi.IViewport;
|
|
36
37
|
|
|
37
38
|
import { VisualFormattingSettingsModel } from "./settings";
|
|
38
39
|
|
|
39
40
|
export class Visual implements IVisual {
|
|
41
|
+
private events: IVisualEventService;
|
|
40
42
|
private imageDiv: HTMLDivElement;
|
|
41
43
|
private imageElement: HTMLImageElement;
|
|
42
44
|
private formattingSettings: VisualFormattingSettingsModel;
|
|
43
45
|
private formattingSettingsService: FormattingSettingsService;
|
|
44
46
|
|
|
45
47
|
public constructor(options: VisualConstructorOptions) {
|
|
48
|
+
this.events = options.host.eventService;
|
|
49
|
+
|
|
46
50
|
this.formattingSettingsService = new FormattingSettingsService();
|
|
47
51
|
this.imageDiv = document.createElement("div");
|
|
48
52
|
this.imageDiv.className = "rcv_autoScaleImageContainer";
|
|
@@ -53,30 +57,40 @@ export class Visual implements IVisual {
|
|
|
53
57
|
}
|
|
54
58
|
|
|
55
59
|
public update(options: VisualUpdateOptions): void {
|
|
56
|
-
|
|
57
|
-
!options.type ||
|
|
58
|
-
!options.viewport ||
|
|
59
|
-
!options.dataViews ||
|
|
60
|
-
options.dataViews.length === 0 ||
|
|
61
|
-
!options.dataViews[0]) {
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
60
|
+
this.events.renderingStarted(options);
|
|
64
61
|
|
|
65
|
-
|
|
66
|
-
|
|
62
|
+
try {
|
|
63
|
+
if (!options ||
|
|
64
|
+
!options.type ||
|
|
65
|
+
!options.viewport ||
|
|
66
|
+
!options.dataViews ||
|
|
67
|
+
options.dataViews.length === 0 ||
|
|
68
|
+
!options.dataViews[0]) {
|
|
69
|
+
this.events.renderingFinished(options);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
67
72
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
imageUrl = "data:image/png;base64," + dataView.scriptResult.payloadBase64;
|
|
71
|
-
}
|
|
73
|
+
const dataView: DataView = options.dataViews[0];
|
|
74
|
+
this.formattingSettings = this.formattingSettingsService.populateFormattingSettingsModel(VisualFormattingSettingsModel, options.dataViews[0]);
|
|
72
75
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
76
|
+
let imageUrl: string = null;
|
|
77
|
+
if (dataView.scriptResult && dataView.scriptResult.payloadBase64) {
|
|
78
|
+
imageUrl = "data:image/png;base64," + dataView.scriptResult.payloadBase64;
|
|
79
|
+
}
|
|
78
80
|
|
|
79
|
-
|
|
81
|
+
if (imageUrl) {
|
|
82
|
+
this.imageElement.src = imageUrl;
|
|
83
|
+
} else {
|
|
84
|
+
this.imageElement.src = null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
this.onResizing(options.viewport);
|
|
88
|
+
this.events.renderingFinished(options);
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
console.log('Error in update method', error);
|
|
92
|
+
this.events.renderingFailed(options, String(error))
|
|
93
|
+
}
|
|
80
94
|
}
|
|
81
95
|
|
|
82
96
|
public onResizing(finalViewport: IViewport): void {
|
|
@@ -32,10 +32,12 @@ import "./../style/visual.less";
|
|
|
32
32
|
import VisualConstructorOptions = powerbi.extensibility.visual.VisualConstructorOptions;
|
|
33
33
|
import VisualUpdateOptions = powerbi.extensibility.visual.VisualUpdateOptions;
|
|
34
34
|
import IVisual = powerbi.extensibility.visual.IVisual;
|
|
35
|
+
import IVisualEventService = powerbi.extensibility.IVisualEventService;
|
|
35
36
|
|
|
36
37
|
import { VisualFormattingSettingsModel } from "./settings";
|
|
37
38
|
|
|
38
39
|
export class Visual implements IVisual {
|
|
40
|
+
private events: IVisualEventService;
|
|
39
41
|
private target: HTMLElement;
|
|
40
42
|
private updateCount: number;
|
|
41
43
|
private textNode: Text;
|
|
@@ -44,6 +46,7 @@ export class Visual implements IVisual {
|
|
|
44
46
|
|
|
45
47
|
constructor(options: VisualConstructorOptions) {
|
|
46
48
|
console.log("Visual constructor", options);
|
|
49
|
+
this.events = options.host.eventService;
|
|
47
50
|
this.formattingSettingsService = new FormattingSettingsService();
|
|
48
51
|
this.target = options.element;
|
|
49
52
|
this.updateCount = 0;
|
|
@@ -59,10 +62,19 @@ export class Visual implements IVisual {
|
|
|
59
62
|
}
|
|
60
63
|
|
|
61
64
|
public update(options: VisualUpdateOptions) {
|
|
62
|
-
this.
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
this.
|
|
65
|
+
this.events.renderingStarted(options);
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
this.formattingSettings = this.formattingSettingsService.populateFormattingSettingsModel(VisualFormattingSettingsModel, options.dataViews[0]);
|
|
69
|
+
console.log("Visual update", options);
|
|
70
|
+
if (this.textNode) {
|
|
71
|
+
this.textNode.textContent = (this.updateCount++).toString();
|
|
72
|
+
}
|
|
73
|
+
this.events.renderingFinished(options);
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
console.log("Visual update error", error);
|
|
77
|
+
this.events.renderingFailed(options, String(error));
|
|
66
78
|
}
|
|
67
79
|
}
|
|
68
80
|
|
|
@@ -37,6 +37,7 @@ import { FormattingSettingsService } from "powerbi-visuals-utils-formattingmodel
|
|
|
37
37
|
import VisualConstructorOptions = powerbi.extensibility.visual.VisualConstructorOptions;
|
|
38
38
|
import VisualUpdateOptions = powerbi.extensibility.visual.VisualUpdateOptions;
|
|
39
39
|
import IVisual = powerbi.extensibility.visual.IVisual;
|
|
40
|
+
import IVisualEventService = powerbi.extensibility.IVisualEventService;
|
|
40
41
|
import DataView = powerbi.DataView;
|
|
41
42
|
import IViewport = powerbi.IViewport;
|
|
42
43
|
|
|
@@ -46,6 +47,7 @@ import { CategoryViewModel, VisualViewModel } from "./visualViewModel";
|
|
|
46
47
|
|
|
47
48
|
"use strict";
|
|
48
49
|
export class Visual implements IVisual {
|
|
50
|
+
private events: IVisualEventService;
|
|
49
51
|
private target: d3.Selection<any, any, any, any>;
|
|
50
52
|
private table: d3.Selection<any, any, any, any>;
|
|
51
53
|
private tHead: d3.Selection<any, any, any, any>;
|
|
@@ -54,6 +56,8 @@ export class Visual implements IVisual {
|
|
|
54
56
|
private formattingSettingsService: FormattingSettingsService;
|
|
55
57
|
|
|
56
58
|
constructor(options: VisualConstructorOptions) {
|
|
59
|
+
this.events = options.host.eventService;
|
|
60
|
+
|
|
57
61
|
this.formattingSettingsService = new FormattingSettingsService();
|
|
58
62
|
|
|
59
63
|
let target: d3.Selection<any, any, any, any> = this.target = select(options.element).append("div")
|
|
@@ -67,7 +71,16 @@ export class Visual implements IVisual {
|
|
|
67
71
|
}
|
|
68
72
|
|
|
69
73
|
public update(options: VisualUpdateOptions): void {
|
|
70
|
-
this.
|
|
74
|
+
this.events.renderingStarted(options);
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
this.updateInternal(options, visualTransform(options.dataViews[0]));
|
|
78
|
+
this.events.renderingFinished(options);
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
console.log("Visual update error", error);
|
|
82
|
+
this.events.renderingFailed(options, String(error));
|
|
83
|
+
}
|
|
71
84
|
}
|
|
72
85
|
|
|
73
86
|
public updateInternal(options: VisualUpdateOptions, viewModel: VisualViewModel): void {
|