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.
@@ -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.2",
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": "^13.1.0",
36
+ "commander": "^14.0.3",
35
37
  "compare-versions": "^6.1.1",
36
- "css-loader": "^7.1.2",
37
- "eslint-plugin-powerbi-visuals": "1.1.0",
38
- "extra-watch-webpack-plugin": "^1.0.3",
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.5.1",
44
- "less-loader": "^12.3.0",
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.9.4",
50
- "powerbi-visuals-webpack-plugin": "^4.3.1",
51
- "terser-webpack-plugin": "^5.3.16",
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.104.1",
55
+ "webpack": "^5.105.4",
55
56
  "webpack-bundle-analyzer": "4.10.2",
56
- "webpack-dev-server": "^5.2.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.50.0",
60
- "eslint": "^9.39.2",
61
- "jasmine": "5.3.1",
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.3",
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": ">=18.0.0"
75
+ "node": ">=20.19.0"
72
76
  }
73
77
  }
@@ -0,0 +1,8 @@
1
+ {
2
+ "servers": {
3
+ "pbiviz": {
4
+ "command": "npx",
5
+ "args": ["-y", "powerbi-visuals-tools", "mcp"]
6
+ }
7
+ }
8
+ }
@@ -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.formattingSettings = this.formattingSettingsService.populateFormattingSettingsModel(VisualFormattingSettingsModel, options.dataViews[0]);
65
+ this.events.renderingStarted(options);
63
66
 
64
- console.log('Visual update', options);
65
- if (this.textNode) {
66
- this.textNode.textContent = (this.updateCount++).toString();
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
- const dataView: DataView = options.dataViews[0];
114
- this.formattingSettings = this.formattingSettingsService.populateFormattingSettingsModel(VisualFormattingSettingsModel, options.dataViews[0]);
121
+ try {
122
+ const dataView: DataView = options.dataViews[0];
123
+ this.formattingSettings = this.formattingSettingsService.populateFormattingSettingsModel(VisualFormattingSettingsModel, options.dataViews[0]);
115
124
 
116
- let payloadBase64: string = null;
117
- if (dataView.scriptResult && dataView.scriptResult.payloadBase64) {
118
- payloadBase64 = dataView.scriptResult.payloadBase64;
119
- }
125
+ let payloadBase64: string = null;
126
+ if (dataView.scriptResult && dataView.scriptResult.payloadBase64) {
127
+ payloadBase64 = dataView.scriptResult.payloadBase64;
128
+ }
120
129
 
121
- if (renderVisualUpdateType.indexOf(options.type) === -1) {
122
- if (payloadBase64) {
123
- this.injectCodeFromPayload(payloadBase64);
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
- } else {
126
- this.onResizing(options.viewport);
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
- if (!options ||
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
- const dataView: DataView = options.dataViews[0];
66
- this.formattingSettings = this.formattingSettingsService.populateFormattingSettingsModel(VisualFormattingSettingsModel, options.dataViews[0]);
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
- let imageUrl: string = null;
69
- if (dataView.scriptResult && dataView.scriptResult.payloadBase64) {
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
- if (imageUrl) {
74
- this.imageElement.src = imageUrl;
75
- } else {
76
- this.imageElement.src = null;
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
- this.onResizing(options.viewport);
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.formattingSettings = this.formattingSettingsService.populateFormattingSettingsModel(VisualFormattingSettingsModel, options.dataViews[0]);
63
- console.log("Visual update", options);
64
- if (this.textNode) {
65
- this.textNode.textContent = (this.updateCount++).toString();
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.updateInternal(options, visualTransform(options.dataViews));
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 {
package/tsconfig.json CHANGED
@@ -14,7 +14,8 @@
14
14
  "ES2022",
15
15
  "dom"
16
16
  ],
17
- "outDir": "lib"
17
+ "outDir": "lib",
18
+ "skipLibCheck": true
18
19
  },
19
20
  "include": [
20
21
  "src/**/*.ts"