├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE.txt ├── NOTICE.txt ├── PowerBI.JavaScript.nuspec ├── README.md ├── SECURITY.md ├── dist ├── powerbi-client.d.ts ├── powerbi.js └── powerbi.min.js ├── gulpfile.js ├── karma.conf.js ├── package.json ├── src ├── FilterBuilders │ ├── advancedFilterBuilder.ts │ ├── basicFilterBuilder.ts │ ├── filterBuilder.ts │ ├── index.ts │ ├── relativeDateFilterBuilder.ts │ ├── relativeTimeFilterBuilder.ts │ └── topNFilterBuilder.ts ├── bookmarksManager.ts ├── config.ts ├── create.ts ├── dashboard.ts ├── embed.ts ├── errors.ts ├── factories.ts ├── ifilterable.ts ├── page.ts ├── powerbi-client.ts ├── qna.ts ├── quickCreate.ts ├── report.ts ├── service.ts ├── tile.ts ├── util.ts ├── visual.ts └── visualDescriptor.ts ├── test ├── SDK-to-HPM.spec.ts ├── SDK-to-MockApp.spec.ts ├── SDK-to-WPMP.spec.ts ├── constsants.ts ├── filterBuilders.spec.ts ├── protocol.spec.ts ├── service.spec.ts ├── test.spec.ts └── utility │ ├── mockApp.ts │ ├── mockEmbed.ts │ ├── mockHpm.ts │ ├── mockRouter.ts │ ├── mockWpmp.ts │ └── noop.html ├── tsconfig.json ├── webpack.config.js ├── webpack.test.config.js └── webpack.test.tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | # Task 512388: Fix eslint warnings and errors in tests 2 | /node_modules/* 3 | /**/*.js 4 | dist/* -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // TODO: Remove "warn" settings for the rules after resolving them 2 | module.exports = { 3 | "env": { 4 | "browser": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "plugin:@typescript-eslint/recommended", 9 | "plugin:@typescript-eslint/recommended-requiring-type-checking" 10 | ], 11 | "parser": "@typescript-eslint/parser", 12 | "parserOptions": { 13 | "project": "webpack.test.tsconfig.json", 14 | "sourceType": "module" 15 | }, 16 | "plugins": [ 17 | "eslint-plugin-jsdoc", 18 | "eslint-plugin-prefer-arrow", 19 | "eslint-plugin-import", 20 | "@typescript-eslint" 21 | ], 22 | "rules": { 23 | "@typescript-eslint/adjacent-overload-signatures": "warn", 24 | "@typescript-eslint/array-type": "off", 25 | "@typescript-eslint/await-thenable": "warn", 26 | "@typescript-eslint/ban-ts-comment": "off", 27 | "@typescript-eslint/ban-types": [ 28 | "warn", 29 | { 30 | "types": { 31 | "Object": { 32 | "message": "Avoid using the `Object` type. Did you mean `object`?" 33 | }, 34 | "Function": false, 35 | "object": false, 36 | "Boolean": { 37 | "message": "Avoid using the `Boolean` type. Did you mean `boolean`?" 38 | }, 39 | "Number": { 40 | "message": "Avoid using the `Number` type. Did you mean `number`?" 41 | }, 42 | "String": { 43 | "message": "Avoid using the `String` type. Did you mean `string`?" 44 | }, 45 | "Symbol": { 46 | "message": "Avoid using the `Symbol` type. Did you mean `symbol`?" 47 | } 48 | } 49 | } 50 | ], 51 | "@typescript-eslint/consistent-type-definitions": "warn", 52 | "@typescript-eslint/dot-notation": "off", 53 | "@typescript-eslint/explicit-member-accessibility": [ 54 | "off", 55 | { 56 | "accessibility": "explicit" 57 | } 58 | ], 59 | "@typescript-eslint/explicit-module-boundary-types": [ 60 | "warn", 61 | { "allowArgumentsExplicitlyTypedAsAny": true } 62 | ], 63 | "@typescript-eslint/indent": [ 64 | "warn", 65 | 2, 66 | { 67 | "SwitchCase": 1, 68 | "FunctionDeclaration": { 69 | "parameters": "first" 70 | }, 71 | "FunctionExpression": { 72 | "parameters": "first" 73 | } 74 | } 75 | ], 76 | "@typescript-eslint/member-delimiter-style": [ 77 | "warn", 78 | { 79 | "multiline": { 80 | "delimiter": "semi", 81 | "requireLast": true 82 | }, 83 | "singleline": { 84 | "delimiter": "semi", 85 | "requireLast": false 86 | } 87 | } 88 | ], 89 | "@typescript-eslint/explicit-function-return-type": [ 90 | "error", 91 | { 92 | "allowExpressions": true, 93 | "allowDirectConstAssertionInArrowFunctions": true 94 | } 95 | ], 96 | "@typescript-eslint/member-ordering": "off", 97 | "@typescript-eslint/naming-convention": "off", 98 | "@typescript-eslint/no-array-constructor": "warn", 99 | "@typescript-eslint/no-empty-function": "warn", 100 | "@typescript-eslint/no-empty-interface": "warn", 101 | "@typescript-eslint/no-explicit-any": "off", 102 | "@typescript-eslint/no-extra-non-null-assertion": "warn", 103 | "@typescript-eslint/no-extra-semi": "warn", 104 | "@typescript-eslint/no-floating-promises": "off", 105 | "@typescript-eslint/no-for-in-array": "warn", 106 | "@typescript-eslint/no-implied-eval": "warn", 107 | "@typescript-eslint/no-inferrable-types": "off", 108 | "@typescript-eslint/no-misused-new": "warn", 109 | "@typescript-eslint/no-misused-promises": "off", 110 | "@typescript-eslint/no-namespace": "warn", 111 | "@typescript-eslint/no-non-null-asserted-optional-chain": "warn", 112 | "@typescript-eslint/no-non-null-assertion": "warn", 113 | "@typescript-eslint/no-parameter-properties": "off", 114 | "@typescript-eslint/no-this-alias": "warn", 115 | "@typescript-eslint/no-unnecessary-type-assertion": "warn", 116 | "@typescript-eslint/no-unsafe-assignment": "off", 117 | "@typescript-eslint/no-unsafe-call": "off", 118 | "@typescript-eslint/no-unsafe-member-access": "off", 119 | "@typescript-eslint/no-unsafe-return": "off", 120 | "@typescript-eslint/no-unused-expressions": "warn", 121 | "@typescript-eslint/no-unused-vars": [ 122 | "warn", 123 | { 124 | "args": "after-used", "argsIgnorePattern": "^_" 125 | } 126 | ], 127 | "@typescript-eslint/no-use-before-define": "off", 128 | "@typescript-eslint/no-var-requires": "warn", 129 | "@typescript-eslint/prefer-as-const": "warn", 130 | "@typescript-eslint/prefer-for-of": "warn", 131 | "@typescript-eslint/prefer-namespace-keyword": "warn", 132 | "@typescript-eslint/prefer-regexp-exec": "off", 133 | "@typescript-eslint/quotes": [ 134 | "off", 135 | { 136 | "avoidEscape": true 137 | } 138 | ], 139 | "@typescript-eslint/require-await": "warn", 140 | "@typescript-eslint/restrict-plus-operands": "warn", 141 | "@typescript-eslint/restrict-template-expressions": "off", 142 | "@typescript-eslint/semi": [ 143 | "error", 144 | ], 145 | "@typescript-eslint/triple-slash-reference": [ 146 | "warn", 147 | { 148 | "path": "always", 149 | "types": "prefer-import", 150 | "lib": "always" 151 | } 152 | ], 153 | "@typescript-eslint/type-annotation-spacing": "warn", 154 | "@typescript-eslint/unbound-method": "off", 155 | "@typescript-eslint/unified-signatures": "warn", 156 | "arrow-parens": "off", 157 | "brace-style": [ 158 | "off", 159 | "1tbs" 160 | ], 161 | "comma-dangle": "off", 162 | "complexity": "off", 163 | "constructor-super": "warn", 164 | "eol-last": "warn", 165 | "eqeqeq": [ 166 | "warn", 167 | "smart" 168 | ], 169 | "guard-for-in": "warn", 170 | "id-blacklist": [ 171 | "warn", 172 | "any", 173 | "Number", 174 | "number", 175 | "String", 176 | "string", 177 | "Boolean", 178 | "boolean", 179 | "Undefined", 180 | ], 181 | "id-match": "warn", 182 | "import/order": "off", 183 | "jsdoc/check-alignment": "warn", 184 | "jsdoc/check-indentation": "warn", 185 | "jsdoc/newline-after-description": "warn", 186 | "max-classes-per-file": [ 187 | "warn", 188 | 1 189 | ], 190 | "max-len": "off", 191 | "new-parens": "warn", 192 | "no-array-constructor": "off", 193 | "no-caller": "warn", 194 | "no-cond-assign": "warn", 195 | "no-console": "off", 196 | "no-debugger": "warn", 197 | "no-empty": "warn", 198 | "no-empty-function": "off", 199 | "no-eval": "warn", 200 | "no-extra-semi": "off", 201 | "no-fallthrough": "off", 202 | "no-implied-eval": "off", 203 | "no-invalid-this": "off", 204 | "no-multiple-empty-lines": ["error", { "max": 1 }], 205 | "no-new-wrappers": "warn", 206 | "no-shadow": "off", 207 | "no-trailing-spaces": "warn", 208 | "no-undef-init": "warn", 209 | "no-underscore-dangle": "off", 210 | "no-unsafe-finally": "warn", 211 | "no-unused-labels": "warn", 212 | "no-var": "warn", 213 | "object-shorthand": "off", 214 | "one-var": "off", 215 | "prefer-const": "off", 216 | "prefer-rest-params": "warn", 217 | "quote-props": [ 218 | "warn", 219 | "consistent-as-needed" 220 | ], 221 | "radix": "warn", 222 | "require-await": "off", 223 | "space-before-function-paren": "off", 224 | "spaced-comment": [ 225 | "warn", 226 | "always", 227 | { 228 | "markers": [ 229 | "/" 230 | ] 231 | } 232 | ], 233 | "use-isnan": "warn", 234 | "valid-typeof": "off" 235 | } 236 | }; 237 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | docs 4 | typings/ 5 | tmp 6 | .publish 7 | npm-debug.log* 8 | dist/powerbi.js.map 9 | *.js.map 10 | package-lock.json 11 | .vscode 12 | owners.txt 13 | test/util.spec.ts 14 | .config/tsaoptions.json -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - node 4 | branches: 5 | except: 6 | - "/^build-[0-9a-z\\-]*/" 7 | deploy: 8 | provider: npm 9 | email: nugetpowerbi@microsoft.com 10 | api_key: 11 | secure: RwWdc+Xlgkm6t1kjOIpopL35tsWTdI01ZC5ygy4DTGNG99sD9+z8PoQFMHWedcgfs+GDJNX8omlxrDVYKjBMRRJ46Y42d3n4nmAydSKvtx8hE2XNiKSyxpzzwdWA15zpwc3t7ijStC3qd8qAZqp+TozykUDO07CSftPv2tj07OK1J5IKIDwHNxJg4vkS9Ab6MOkfHHicEUYk7g0Yey1TmsXUx+tnlzPrx8pxrmxok7jmgm5k5qGa+otw+UPWnKMSL+PhdFBjk/CfEnmDrgqpP41fA7J5tOCeNG1HhV8UnH+ChuIz3WHFbM9AoUeDf7e3wdev4TZ5mk4o2NJgWL6Sj1aTDbMJOnmtun+rZUqy0ph5PD5yieV31HlYQBXLhzph8LLL5Ejo0wdUohxni4Dnn1R0GoI2AFWbq7EVwmO6COVAIEw+lnGvNCEgMmBhkM1QrV5BYVgOiTXYyPVv/9VBbVcQeD+kN/Z/x5FyxB1YaD5sJV5wZRmOfFSTSnKiSwVX6Fd++/3gcWpX57jXBsT4E9Bw8oMxlPmHGogzm4VJP/tShQG/N3l8pqP4JcsozoSxPHP07aDhphh3Nr2c8Gz48z5ggv8QEuvxzwn8aT1c3o7OO7niGWbMTXvN3oxx4t0jE8KsrvkGA8w67D3F7RnRZFOEDJKz3/dfAJCQlGeIKcU= 12 | on: 13 | tags: true 14 | repo: Microsoft/PowerBI-JavaScript 15 | branch: dev 16 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Setup 4 | 5 | Clone the repository: 6 | ``` 7 | git clone https://github.com/Microsoft/PowerBI-JavaScript.git powerbi-client 8 | ``` 9 | 10 | Navigate to the cloned directory 11 | 12 | Install local dependencies: 13 | ``` 14 | npm install 15 | ``` 16 | 17 | ## Build: 18 | ``` 19 | npm run build 20 | ``` 21 | Or if using VScode: `Ctrl + Shift + B` 22 | 23 | ## Test 24 | ``` 25 | npm test 26 | ``` 27 | By default the tests run using PhantomJS 28 | 29 | There are various command line arguments that can be passed to the test command to facilitate debugging: 30 | 31 | Run tests with Chrome 32 | ``` 33 | npm test -- --chrome 34 | ``` 35 | 36 | Enable debug level logging for karma, and remove code coverage 37 | ``` 38 | npm test -- --debug 39 | ``` 40 | 41 | Disable single run to remain open for debugging 42 | ``` 43 | npm test -- --watch 44 | ``` 45 | 46 | These are often combined and typical command for debugging tests is: 47 | ``` 48 | npm test -- --chrome --debug --watch 49 | ``` 50 | 51 | You can debug by directly calling karma: 52 | ``` 53 | node node_modules/karma/bin/karma start --browsers=Firefox --single-run=false --watch 54 | ``` 55 | 56 | The build and tests use webpack to compile all the source modules into one bundled module that can execute in the browser. 57 | 58 | ## Updating the documentation (For those with push permissions only) 59 | First run the command to build the docs and open it to verify the changes are as expected. 60 | 61 | ``` 62 | npm run gulp -- build:docs 63 | ``` 64 | > There are errors during the TypeDoc compilation step due to some complication with modules however the documentation should still be generated. It's not clear if these are fixable by including more src files in the gulp task or if it's just the nature of TypeDoc lacking capabilities for this project structure. 65 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Microsoft.PowerBI.JavaScript 2 | 3 | Copyright (c) Microsoft Corporation 4 | 5 | All rights reserved. 6 | 7 | MIT License 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 14 | -------------------------------------------------------------------------------- /NOTICE.txt: -------------------------------------------------------------------------------- 1 | Microsoft.PowerBI.JavaScript 2 | 3 | THIRD-PARTY SOFTWARE NOTICES AND INFORMATION 4 | Do Not Translate or Localize 5 | 6 | This project incorporates components from the projects listed below. The original copyright notices and the licenses under which Microsoft received such components are set forth below. Microsoft reserves all rights not expressly granted herein, whether by implication, estoppel or otherwise. 7 | 8 | 1. SyntaxHighlighter (https://github.com/syntaxhighlighter/syntaxhighlighter) 9 | 10 | Copyright (c) 2004-2013, Alex Gorbatchev 11 | 12 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | -------------------------------------------------------------------------------- /PowerBI.JavaScript.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Microsoft.PowerBI.JavaScript 5 | $version$ 6 | Microsoft 7 | microsoft,powerbi 8 | https://raw.githubusercontent.com/Microsoft/dotnet/master/LICENSE 9 | https://github.com/Microsoft/PowerBI-JavaScript 10 | http://go.microsoft.com/fwlink/?LinkId=780675 11 | true 12 | JavaScript web components for Power BI 13 | A suite of JavaScript web components for integrating Power BI into your app 14 | © Microsoft Corporation. All rights reserved. 15 | Microsoft Power BI JavaScript JS 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # powerbi-client 2 | A client side library for embedding Power BI using JavaScript or TypeScript into your apps. 3 | 4 | [![Build Status](https://img.shields.io/travis/Microsoft/PowerBI-JavaScript/master.svg)](https://travis-ci.org/Microsoft/PowerBI-JavaScript) 5 | [![NPM Version](https://img.shields.io/npm/v/powerbi-client.svg)](https://www.npmjs.com/package/powerbi-client) 6 | [![Nuget Version](https://img.shields.io/nuget/v/Microsoft.PowerBI.JavaScript.svg)](https://www.nuget.org/packages/Microsoft.PowerBI.JavaScript/) 7 | [![NPM Total Downloads](https://img.shields.io/npm/dt/powerbi-client.svg)](https://www.npmjs.com/package/powerbi-client) 8 | [![NPM Monthly Downloads](https://img.shields.io/npm/dm/powerbi-client.svg)](https://www.npmjs.com/package/powerbi-client) 9 | [![GitHub tag](https://img.shields.io/github/tag/microsoft/powerbi-javascript.svg)](https://github.com/Microsoft/PowerBI-JavaScript/tags) 10 | [![Gitter](https://img.shields.io/gitter/room/Microsoft/PowerBI-JavaScript.svg)](https://gitter.im/Microsoft/PowerBI-JavaScript) 11 | 12 | ## Documentation 13 | See the [Power BI embedded analytics Client APIs documentation](https://docs.microsoft.com/javascript/api/overview/powerbi/) to learn how to embed a Power BI report in your application and how to use the client APIs. 14 | 15 | ## Code Docs 16 | See the [code docs](https://learn.microsoft.com/en-us/javascript/api/powerbi/powerbi-client) for detailed information about classes, interfaces, types, etc. 17 | 18 | ## Sample Application 19 | For examples of applications utilizing the `powerbi-client` library, please refer to the available samples in the [PowerBI-Developer-Samples repository](https://github.com/microsoft/PowerBI-Developer-Samples). 20 | 21 | ## Playground 22 | To explore and understand the capabilities of embedded analytics in your applications, please visit the [Power BI Embedded Analytics Playground](https://playground.powerbi.com). 23 | 24 | ## Installation 25 | 26 | Install via Nuget: 27 | 28 | `Install-Package Microsoft.PowerBI.JavaScript` 29 | 30 | Install from NPM: 31 | 32 | `npm install --save powerbi-client` 33 | 34 | Installing beta versions: 35 | 36 | `npm install --save powerbi-client@beta` 37 | 38 | ## Include the library via import or manually 39 | 40 | Ideally you would use a module loader or a compilation step to import using ES6 modules as: 41 | 42 | ```javascript 43 | import * as pbi from 'powerbi-client'; 44 | ``` 45 | 46 | However, the library is exported as a Universal Module and the powerbi.js script can be included before your app's closing `` tag as: 47 | 48 | ```html 49 | 50 | ``` 51 | 52 | When included directly, the library is exposed as a global named `powerbi-client`. 53 | There is also another global named `powerbi` which is an instance of the service. 54 | 55 | ## Contributing 56 | 57 | This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. For details, visit . 58 | 59 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions provided by the bot. You will only need to do this once across all repos using our CLA. 60 | 61 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). 40 | 41 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const gulp = require('gulp'); 3 | const prepend = require('gulp-prepend'); 4 | const help = require('gulp-help-four'); 5 | const rename = require('gulp-rename'); 6 | const replace = require('gulp-replace'); 7 | const eslint = require("gulp-eslint"); 8 | const ts = require('gulp-typescript'); 9 | const flatten = require('gulp-flatten'); 10 | const del = require('del'); 11 | const karma = require('karma'); 12 | const typedoc = require('gulp-typedoc'); 13 | const watch = require('gulp-watch'); 14 | const webpack = require('webpack'); 15 | const webpackStream = require('webpack-stream'); 16 | const runSequence = require('gulp4-run-sequence'); 17 | const webpackConfig = require('./webpack.config'); 18 | const webpackTestConfig = require('./webpack.test.config'); 19 | const argv = require('yargs').argv; 20 | 21 | help(gulp, undefined); 22 | 23 | const package = require('./package.json'); 24 | const webpackBanner = "// " + package.name + " v" + package.version + "\n" 25 | + "// Copyright (c) Microsoft Corporation.\n" 26 | + "// Licensed under the MIT License."; 27 | const banner = webpackBanner + "\n"; 28 | 29 | gulp.task("docs", 'Compile documentation from src code', function () { 30 | return gulp 31 | .src([ 32 | "node_modules/es6-promise/es6-promise.d.ts", 33 | "node_modules/powerbi-models/dist/models.d.ts", 34 | "src/**/*.ts" 35 | ]) 36 | .pipe(typedoc({ 37 | mode: 'modules', 38 | includeDeclarations: true, 39 | 40 | // Output options (see typedoc docs) 41 | out: "./docs", 42 | json: "./docs/json/" + package.name + ".json", 43 | 44 | // TypeDoc options (see typedoc docs) 45 | ignoreCompilerErrors: true, 46 | version: true 47 | })); 48 | }); 49 | 50 | gulp.task('nojekyll', 'Add .nojekyll file to docs directory', function (done) { 51 | fs.writeFile('./docs/.nojekyll', '', function (error) { 52 | if (error) { 53 | throw error; 54 | } 55 | 56 | done(); 57 | }); 58 | }); 59 | 60 | gulp.task('lint', 'Lints all files', function (done) { 61 | runSequence( 62 | 'lint:ts', 63 | done 64 | ); 65 | }); 66 | 67 | gulp.task('test', 'Runs all tests', function (done) { 68 | runSequence( 69 | 'lint:ts', 70 | 'config', 71 | 'compile:spec', 72 | 'test:js', 73 | done 74 | ); 75 | }); 76 | 77 | gulp.task('build', 'Runs a full build', function (done) { 78 | runSequence( 79 | 'lint:ts', 80 | 'clean', 81 | 'config', 82 | ['compile:ts', 'compile:dts'], 83 | 'min:js', 84 | 'prepend', 85 | done 86 | ); 87 | }); 88 | 89 | gulp.task('build:docs', 'Build docs folder', function (done) { 90 | return runSequence( 91 | 'clean:docs', 92 | 'docs', 93 | 'nojekyll', 94 | done 95 | ); 96 | }); 97 | 98 | gulp.task('config', 'Update config version with package version', function () { 99 | return gulp.src(['./src/config.ts'], { base: "./" }) 100 | .pipe(replace(/version: '([^']+)'/, `version: '${package.version}'`)) 101 | .pipe(gulp.dest('.')); 102 | }); 103 | 104 | gulp.task('prepend', 'Add header to distributed files', function () { 105 | return gulp.src(['./dist/*.d.ts']) 106 | .pipe(prepend(banner)) 107 | .pipe(gulp.dest('./dist')); 108 | }); 109 | 110 | gulp.task('clean', 'Cleans destination folder', function (done) { 111 | return del([ 112 | './dist/**/*' 113 | ]); 114 | }); 115 | 116 | gulp.task('clean:docs', 'Clean docs directory', function () { 117 | return del([ 118 | 'docs/**/*', 119 | 'docs' 120 | ]); 121 | }); 122 | 123 | gulp.task('lint:ts', 'Lints all TypeScript', function () { 124 | return gulp.src(['./src/**/*.ts', './test/**/*.ts']) 125 | .pipe(eslint({ 126 | formatter: "verbose" 127 | })) 128 | .pipe(eslint.format()); 129 | }); 130 | 131 | gulp.task('min:js', 'Creates minified JavaScript file', function () { 132 | webpackConfig.plugins = [ 133 | new webpack.BannerPlugin({ 134 | banner: webpackBanner, 135 | raw: true 136 | }) 137 | ]; 138 | 139 | // Create minified bundle without source map 140 | webpackConfig.mode = 'production'; 141 | webpackConfig.devtool = false; 142 | 143 | return gulp.src(['./src/powerbi-client.ts']) 144 | .pipe(webpackStream({ 145 | config: webpackConfig 146 | })) 147 | .pipe(prepend(banner)) 148 | .pipe(rename({ 149 | suffix: '.min' 150 | })) 151 | .pipe(gulp.dest('dist/')); 152 | }); 153 | 154 | gulp.task('compile:ts', 'Compile typescript for powerbi-client library', function () { 155 | webpackConfig.plugins = [ 156 | new webpack.BannerPlugin({ 157 | banner: webpackBanner, 158 | raw: true 159 | }) 160 | ]; 161 | webpackConfig.mode = "development"; 162 | 163 | return gulp.src(['./src/powerbi-client.ts']) 164 | .pipe(webpackStream(webpackConfig)) 165 | .pipe(gulp.dest('dist/')); 166 | }); 167 | 168 | gulp.task('compile:dts', 'Generate one dts file from modules', function () { 169 | const tsProject = ts.createProject('tsconfig.json', { 170 | declaration: true, 171 | sourceMap: false 172 | }); 173 | 174 | const settings = { 175 | out: "powerbi-client.js", 176 | declaration: true, 177 | module: "system", 178 | moduleResolution: "node" 179 | }; 180 | 181 | const tsResult = tsProject.src() 182 | .pipe(ts(settings)); 183 | 184 | return tsResult.dts 185 | .pipe(flatten()) 186 | .pipe(gulp.dest('./dist')); 187 | }); 188 | 189 | gulp.task('compile:spec', 'Compile spec tests', function () { 190 | return gulp.src(['./test/**/*.ts']) 191 | .pipe(webpackStream(webpackTestConfig)) 192 | .pipe(gulp.dest('./tmp')); 193 | }); 194 | 195 | gulp.task('test:js', 'Run js tests', function (done) { 196 | new karma.Server({ 197 | configFile: __dirname + '/karma.conf.js', 198 | singleRun: argv.watch ? false : true, 199 | captureTimeout: argv.timeout || 60000 200 | }, function (exitStatus) { 201 | done(); 202 | //process.exit(exitStatus); TODO: Return it back after migration from PhantomJS to chromeHeadless 203 | }) 204 | .on('browser_register', (browser) => { 205 | if (argv.chrome) { 206 | browser.socket.on('disconnect', function (reason) { 207 | if (reason === "transport close" || reason === "transport error") { 208 | done(0); 209 | process.exit(0); 210 | } 211 | }); 212 | } 213 | }) 214 | .start(); 215 | if (argv.chrome) { 216 | return watch(["src/**/*.ts", "test/**/*.ts"], function () { 217 | runSequence('compile:spec'); 218 | }); 219 | } 220 | }); 221 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | var argv = require('yargs').argv; 2 | 3 | var browserName = 'Chrome_headless'; 4 | if (argv.chrome) { 5 | browserName = 'Chrome_headless' 6 | } 7 | else if (argv.firefox) { 8 | browserName = 'Firefox' 9 | } 10 | const flags = [ 11 | '--disable-extensions', 12 | '--no-proxy-server', 13 | '--js-flags="--max_old_space_size=6500"', 14 | '--high-dpi-support=1', 15 | ]; 16 | module.exports = function (config) { 17 | config.set({ 18 | frameworks: ['jasmine'], 19 | files: [ 20 | './node_modules/jquery/dist/jquery.js', 21 | './node_modules/es6-promise/dist/es6-promise.js', 22 | './tmp/**/*.js', 23 | { pattern: './test/**/*.html', served: true, included: false } 24 | ], 25 | exclude: [], 26 | reporters: argv.chrome ? ['kjhtml'] : ['spec', 'junit'], 27 | autoWatch: true, 28 | browsers: [browserName], 29 | browserNoActivityTimeout: 300000, 30 | plugins: [ 31 | 'karma-firefox-launcher', 32 | 'karma-chrome-launcher', 33 | 'karma-jasmine', 34 | 'karma-spec-reporter', 35 | 'karma-jasmine-html-reporter', 36 | 'karma-junit-reporter' 37 | ], 38 | customLaunchers: { 39 | 'Chrome_headless': { 40 | base: 'Chrome', 41 | flags: flags.concat("--no-sandbox", "--window-size=800,800"), 42 | }, 43 | }, 44 | junitReporter: { 45 | outputDir: 'tmp', 46 | outputFile: 'testresults.xml', 47 | useBrowserName: false 48 | }, 49 | retryLimit: 0, 50 | logLevel: argv.debug ? config.LOG_DEBUG : config.LOG_INFO, 51 | client: { 52 | clearContext: false, // leave Jasmine Spec Runner output visible in browser 53 | } 54 | }); 55 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "powerbi-client", 3 | "version": "2.23.1", 4 | "description": "JavaScript library for embedding Power BI into your apps. Provides service which makes it easy to embed different types of components and an object model which allows easy interaction with these components such as changing pages, applying filters, and responding to data selection.", 5 | "main": "dist/powerbi.js", 6 | "types": "dist/powerbi-client.d.ts", 7 | "files": [ 8 | "dist" 9 | ], 10 | "directories": { 11 | "test": "test" 12 | }, 13 | "scripts": { 14 | "build": "gulp build", 15 | "test": "gulp test", 16 | "gulp": "gulp", 17 | "tests": "npm test -- --chrome --watch" 18 | }, 19 | "keywords": [ 20 | "microsoft", 21 | "powerbi", 22 | "embedded", 23 | "visuals" 24 | ], 25 | "repository": { 26 | "type": "git", 27 | "url": "https://github.com/Microsoft/PowerBI-JavaScript.git" 28 | }, 29 | "author": "Microsoft", 30 | "license": "MIT", 31 | "devDependencies": { 32 | "@types/jasmine": "^3.5.10", 33 | "@types/jquery": "^3.3.34", 34 | "@types/jsen": "0.0.19", 35 | "@types/karma-jasmine": "^3.1.0", 36 | "@types/node": "^14.14.7", 37 | "@types/npm": "^2.0.31", 38 | "@typescript-eslint/eslint-plugin": "^4.7.0", 39 | "@typescript-eslint/parser": "^4.7.0", 40 | "del": "^2.2.2", 41 | "es6-promise": "^3.2.1", 42 | "eslint": "^7.13.0", 43 | "eslint-plugin-import": "^2.22.1", 44 | "eslint-plugin-jsdoc": "^30.7.7", 45 | "eslint-plugin-prefer-arrow": "^1.2.2", 46 | "gulp": "^4.0.2", 47 | "gulp-eslint": "^6.0.0", 48 | "gulp-flatten": "^0.4.0", 49 | "gulp-prepend": "^0.3.0", 50 | "gulp-help-four": "^0.2.3", 51 | "gulp-rename": "^1.2.2", 52 | "gulp-replace": "^0.5.4", 53 | "gulp-typedoc": "^2.0.0", 54 | "gulp-typescript": "^6.0.0-alpha.1", 55 | "gulp-watch": "^5.0.1", 56 | "gulp4-run-sequence": "^1.0.0", 57 | "http-server": "^14.1.1", 58 | "ignore-loader": "^0.1.1", 59 | "jasmine-core": "3.10.1", 60 | "jquery": "^3.3.1", 61 | "json-loader": "^0.5.4", 62 | "karma": "^6.3.5", 63 | "karma-chrome-launcher": "^3.1.0", 64 | "karma-firefox-launcher": "^1.2.0", 65 | "karma-jasmine": "4.0.1", 66 | "karma-jasmine-html-reporter": "1.7.0", 67 | "karma-junit-reporter": "^2.0.1", 68 | "karma-spec-reporter": "0.0.32", 69 | "ts-loader": "^6.2.2", 70 | "typedoc": "^0.23.23", 71 | "typescript": "~4.6.0", 72 | "webpack": "^5.75.0", 73 | "webpack-stream": "^7.0.0", 74 | "yargs": "^16.1.0" 75 | }, 76 | "dependencies": { 77 | "http-post-message": "^0.2", 78 | "powerbi-models": "^1.14.0", 79 | "powerbi-router": "^0.1", 80 | "window-post-message-proxy": "^0.2.7" 81 | }, 82 | "publishConfig": { 83 | "tag": "beta" 84 | }, 85 | "overrides": { 86 | "glob-parent": "^6.0.2", 87 | "micromatch": "^4.0.5", 88 | "braces": "3.0.3", 89 | "ws": "8.17.1" 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/FilterBuilders/advancedFilterBuilder.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | import { 5 | AdvancedFilter, 6 | AdvancedFilterLogicalOperators, 7 | IAdvancedFilterCondition, 8 | AdvancedFilterConditionOperators 9 | } from "powerbi-models"; 10 | 11 | import { FilterBuilder } from './filterBuilder'; 12 | 13 | /** 14 | * Power BI Advanced filter builder component 15 | * 16 | * @export 17 | * @class AdvancedFilterBuilder 18 | * @extends {FilterBuilder} 19 | */ 20 | export class AdvancedFilterBuilder extends FilterBuilder { 21 | 22 | private logicalOperator: AdvancedFilterLogicalOperators; 23 | private conditions: IAdvancedFilterCondition[] = []; 24 | 25 | /** 26 | * Sets And as logical operator for Advanced filter 27 | * 28 | * ```javascript 29 | * 30 | * const advancedFilterBuilder = new AdvancedFilterBuilder().and(); 31 | * ``` 32 | * 33 | * @returns {AdvancedFilterBuilder} 34 | */ 35 | and(): AdvancedFilterBuilder { 36 | this.logicalOperator = "And"; 37 | return this; 38 | } 39 | 40 | /** 41 | * Sets Or as logical operator for Advanced filter 42 | * 43 | * ```javascript 44 | * 45 | * const advancedFilterBuilder = new AdvancedFilterBuilder().or(); 46 | * ``` 47 | * 48 | * @returns {AdvancedFilterBuilder} 49 | */ 50 | or(): AdvancedFilterBuilder { 51 | this.logicalOperator = "Or"; 52 | return this; 53 | } 54 | 55 | /** 56 | * Adds a condition in Advanced filter 57 | * 58 | * ```javascript 59 | * 60 | * // Add two conditions 61 | * const advancedFilterBuilder = new AdvancedFilterBuilder().addCondition("Contains", "Wash").addCondition("Contains", "Park"); 62 | * ``` 63 | * 64 | * @returns {AdvancedFilterBuilder} 65 | */ 66 | addCondition(operator: AdvancedFilterConditionOperators, value?: (string | number | boolean | Date)): AdvancedFilterBuilder { 67 | const condition: IAdvancedFilterCondition = { 68 | operator: operator, 69 | value: value 70 | }; 71 | this.conditions.push(condition); 72 | return this; 73 | } 74 | 75 | /** 76 | * Creates Advanced filter 77 | * 78 | * ```javascript 79 | * 80 | * const advancedFilterBuilder = new AdvancedFilterBuilder().build(); 81 | * ``` 82 | * 83 | * @returns {AdvancedFilter} 84 | */ 85 | build(): AdvancedFilter { 86 | const advancedFilter = new AdvancedFilter(this.target, this.logicalOperator, this.conditions); 87 | return advancedFilter; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/FilterBuilders/basicFilterBuilder.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | import { 5 | BasicFilter, 6 | BasicFilterOperators 7 | } from "powerbi-models"; 8 | 9 | import { FilterBuilder } from './filterBuilder'; 10 | 11 | /** 12 | * Power BI Basic filter builder component 13 | * 14 | * @export 15 | * @class BasicFilterBuilder 16 | * @extends {FilterBuilder} 17 | */ 18 | export class BasicFilterBuilder extends FilterBuilder { 19 | 20 | private values: Array<(string | number | boolean)>; 21 | private operator: BasicFilterOperators; 22 | private isRequireSingleSelection = false; 23 | 24 | /** 25 | * Sets In as operator for Basic filter 26 | * 27 | * ```javascript 28 | * 29 | * const basicFilterBuilder = new BasicFilterBuilder().in([values]); 30 | * ``` 31 | * 32 | * @returns {BasicFilterBuilder} 33 | */ 34 | in(values: Array<(string | number | boolean)>): BasicFilterBuilder { 35 | this.operator = "In"; 36 | this.values = values; 37 | return this; 38 | } 39 | 40 | /** 41 | * Sets NotIn as operator for Basic filter 42 | * 43 | * ```javascript 44 | * 45 | * const basicFilterBuilder = new BasicFilterBuilder().notIn([values]); 46 | * ``` 47 | * 48 | * @returns {BasicFilterBuilder} 49 | */ 50 | notIn(values: Array<(string | number | boolean)>): BasicFilterBuilder { 51 | this.operator = "NotIn"; 52 | this.values = values; 53 | return this; 54 | } 55 | 56 | /** 57 | * Sets All as operator for Basic filter 58 | * 59 | * ```javascript 60 | * 61 | * const basicFilterBuilder = new BasicFilterBuilder().all(); 62 | * ``` 63 | * 64 | * @returns {BasicFilterBuilder} 65 | */ 66 | all(): BasicFilterBuilder { 67 | this.operator = "All"; 68 | this.values = []; 69 | return this; 70 | } 71 | 72 | /** 73 | * Sets required single selection property for Basic filter 74 | * 75 | * ```javascript 76 | * 77 | * const basicFilterBuilder = new BasicFilterBuilder().requireSingleSelection(isRequireSingleSelection); 78 | * ``` 79 | * 80 | * @returns {BasicFilterBuilder} 81 | */ 82 | requireSingleSelection(isRequireSingleSelection = false): BasicFilterBuilder { 83 | this.isRequireSingleSelection = isRequireSingleSelection; 84 | return this; 85 | } 86 | 87 | /** 88 | * Creates Basic filter 89 | * 90 | * ```javascript 91 | * 92 | * const basicFilterBuilder = new BasicFilterBuilder().build(); 93 | * ``` 94 | * 95 | * @returns {BasicFilter} 96 | */ 97 | build(): BasicFilter { 98 | const basicFilter = new BasicFilter(this.target, this.operator, this.values); 99 | basicFilter.requireSingleSelection = this.isRequireSingleSelection; 100 | return basicFilter; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/FilterBuilders/filterBuilder.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | import { IFilterTarget } from "powerbi-models"; 5 | 6 | /** 7 | * Generic filter builder for BasicFilter, AdvancedFilter, RelativeDate, RelativeTime and TopN 8 | * 9 | * @class 10 | */ 11 | export class FilterBuilder { 12 | 13 | public target: IFilterTarget; 14 | 15 | /** 16 | * Sets target property for filter with target object 17 | * 18 | * ```javascript 19 | * const target = { 20 | * table: 'table1', 21 | * column: 'column1' 22 | * }; 23 | * 24 | * const filterBuilder = new FilterBuilder().withTargetObject(target); 25 | * ``` 26 | * 27 | * @returns {FilterBuilder} 28 | */ 29 | withTargetObject(target: IFilterTarget): this { 30 | this.target = target; 31 | return this; 32 | } 33 | 34 | /** 35 | * Sets target property for filter with column target object 36 | * 37 | * ``` 38 | * const filterBuilder = new FilterBuilder().withColumnTarget(tableName, columnName); 39 | * ``` 40 | * 41 | * @returns {FilterBuilder} 42 | */ 43 | withColumnTarget(tableName: string, columnName: string): this { 44 | this.target = { table: tableName, column: columnName }; 45 | return this; 46 | } 47 | 48 | /** 49 | * Sets target property for filter with measure target object 50 | * 51 | * ``` 52 | * const filterBuilder = new FilterBuilder().withMeasureTarget(tableName, measure); 53 | * ``` 54 | * 55 | * @returns {FilterBuilder} 56 | */ 57 | withMeasureTarget(tableName: string, measure: string): this { 58 | this.target = { table: tableName, measure: measure }; 59 | return this; 60 | } 61 | 62 | /** 63 | * Sets target property for filter with hierarchy level target object 64 | * 65 | * ``` 66 | * const filterBuilder = new FilterBuilder().withHierarchyLevelTarget(tableName, hierarchy, hierarchyLevel); 67 | * ``` 68 | * 69 | * @returns {FilterBuilder} 70 | */ 71 | withHierarchyLevelTarget(tableName: string, hierarchy: string, hierarchyLevel: string): this { 72 | this.target = { table: tableName, hierarchy: hierarchy, hierarchyLevel: hierarchyLevel }; 73 | return this; 74 | } 75 | 76 | /** 77 | * Sets target property for filter with column aggregation target object 78 | * 79 | * ``` 80 | * const filterBuilder = new FilterBuilder().withColumnAggregation(tableName, columnName, aggregationFunction); 81 | * ``` 82 | * 83 | * @returns {FilterBuilder} 84 | */ 85 | withColumnAggregation(tableName: string, columnName: string, aggregationFunction: string): this { 86 | this.target = { table: tableName, column: columnName, aggregationFunction: aggregationFunction }; 87 | return this; 88 | } 89 | 90 | /** 91 | * Sets target property for filter with hierarchy level aggregation target object 92 | * 93 | * ``` 94 | * const filterBuilder = new FilterBuilder().withHierarchyLevelAggregationTarget(tableName, hierarchy, hierarchyLevel, aggregationFunction); 95 | * ``` 96 | * 97 | * @returns {FilterBuilder} 98 | */ 99 | withHierarchyLevelAggregationTarget(tableName: string, hierarchy: string, hierarchyLevel: string, aggregationFunction: string): this { 100 | this.target = { table: tableName, hierarchy: hierarchy, hierarchyLevel: hierarchyLevel, aggregationFunction: aggregationFunction }; 101 | return this; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/FilterBuilders/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | export { 5 | BasicFilterBuilder 6 | } from "./basicFilterBuilder"; 7 | export { 8 | AdvancedFilterBuilder 9 | } from "./advancedFilterBuilder"; 10 | export { 11 | TopNFilterBuilder 12 | } from "./topNFilterBuilder"; 13 | export { 14 | RelativeDateFilterBuilder 15 | } from "./relativeDateFilterBuilder"; 16 | export { 17 | RelativeTimeFilterBuilder 18 | } from "./relativeTimeFilterBuilder"; 19 | -------------------------------------------------------------------------------- /src/FilterBuilders/relativeDateFilterBuilder.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | import { 5 | RelativeDateFilter, 6 | RelativeDateOperators, 7 | RelativeDateFilterTimeUnit 8 | } from "powerbi-models"; 9 | 10 | import { FilterBuilder } from './filterBuilder'; 11 | 12 | /** 13 | * Power BI Relative Date filter builder component 14 | * 15 | * @export 16 | * @class RelativeDateFilterBuilder 17 | * @extends {FilterBuilder} 18 | */ 19 | export class RelativeDateFilterBuilder extends FilterBuilder { 20 | 21 | private operator: RelativeDateOperators; 22 | private timeUnitsCount: number; 23 | private timeUnitType: RelativeDateFilterTimeUnit; 24 | private isTodayIncluded = true; 25 | 26 | /** 27 | * Sets inLast as operator for Relative Date filter 28 | * 29 | * ```javascript 30 | * 31 | * const relativeDateFilterBuilder = new RelativeDateFilterBuilder().inLast(timeUnitsCount, timeUnitType); 32 | * ``` 33 | * 34 | * @param {number} timeUnitsCount - The amount of time units 35 | * @param {RelativeDateFilterTimeUnit} timeUnitType - Defines the unit of time the filter is using 36 | * @returns {RelativeDateFilterBuilder} 37 | */ 38 | inLast(timeUnitsCount: number, timeUnitType: RelativeDateFilterTimeUnit): RelativeDateFilterBuilder { 39 | this.operator = RelativeDateOperators.InLast; 40 | this.timeUnitsCount = timeUnitsCount; 41 | this.timeUnitType = timeUnitType; 42 | return this; 43 | } 44 | 45 | /** 46 | * Sets inThis as operator for Relative Date filter 47 | * 48 | * ```javascript 49 | * 50 | * const relativeDateFilterBuilder = new RelativeDateFilterBuilder().inThis(timeUnitsCount, timeUnitType); 51 | * ``` 52 | * 53 | * @param {number} timeUnitsCount - The amount of time units 54 | * @param {RelativeDateFilterTimeUnit} timeUnitType - Defines the unit of time the filter is using 55 | * @returns {RelativeDateFilterBuilder} 56 | */ 57 | inThis(timeUnitsCount: number, timeUnitType: RelativeDateFilterTimeUnit): RelativeDateFilterBuilder { 58 | this.operator = RelativeDateOperators.InThis; 59 | this.timeUnitsCount = timeUnitsCount; 60 | this.timeUnitType = timeUnitType; 61 | return this; 62 | } 63 | 64 | /** 65 | * Sets inNext as operator for Relative Date filter 66 | * 67 | * ```javascript 68 | * 69 | * const relativeDateFilterBuilder = new RelativeDateFilterBuilder().inNext(timeUnitsCount, timeUnitType); 70 | * ``` 71 | * 72 | * @param {number} timeUnitsCount - The amount of time units 73 | * @param {RelativeDateFilterTimeUnit} timeUnitType - Defines the unit of time the filter is using 74 | * @returns {RelativeDateFilterBuilder} 75 | */ 76 | inNext(timeUnitsCount: number, timeUnitType: RelativeDateFilterTimeUnit): RelativeDateFilterBuilder { 77 | this.operator = RelativeDateOperators.InNext; 78 | this.timeUnitsCount = timeUnitsCount; 79 | this.timeUnitType = timeUnitType; 80 | return this; 81 | } 82 | 83 | /** 84 | * Sets includeToday for Relative Date filter 85 | * 86 | * ```javascript 87 | * 88 | * const relativeDateFilterBuilder = new RelativeDateFilterBuilder().includeToday(includeToday); 89 | * ``` 90 | * 91 | * @param {boolean} includeToday - Denotes if today is included or not 92 | * @returns {RelativeDateFilterBuilder} 93 | */ 94 | includeToday(includeToday: boolean): RelativeDateFilterBuilder { 95 | this.isTodayIncluded = includeToday; 96 | return this; 97 | } 98 | 99 | /** 100 | * Creates Relative Date filter 101 | * 102 | * ```javascript 103 | * 104 | * const relativeDateFilterBuilder = new RelativeDateFilterBuilder().build(); 105 | * ``` 106 | * 107 | * @returns {RelativeDateFilter} 108 | */ 109 | build(): RelativeDateFilter { 110 | const relativeDateFilter = new RelativeDateFilter(this.target, this.operator, this.timeUnitsCount, this.timeUnitType, this.isTodayIncluded); 111 | return relativeDateFilter; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/FilterBuilders/relativeTimeFilterBuilder.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | import { 5 | RelativeTimeFilter, 6 | RelativeDateOperators, 7 | RelativeDateFilterTimeUnit 8 | } from "powerbi-models"; 9 | 10 | import { FilterBuilder } from './filterBuilder'; 11 | 12 | /** 13 | * Power BI Relative Time filter builder component 14 | * 15 | * @export 16 | * @class RelativeTimeFilterBuilder 17 | * @extends {FilterBuilder} 18 | */ 19 | export class RelativeTimeFilterBuilder extends FilterBuilder { 20 | 21 | private operator: RelativeDateOperators; 22 | private timeUnitsCount: number; 23 | private timeUnitType: RelativeDateFilterTimeUnit; 24 | 25 | /** 26 | * Sets inLast as operator for Relative Time filter 27 | * 28 | * ```javascript 29 | * 30 | * const relativeTimeFilterBuilder = new RelativeTimeFilterBuilder().inLast(timeUnitsCount, timeUnitType); 31 | * ``` 32 | * 33 | * @param {number} timeUnitsCount - The amount of time units 34 | * @param {RelativeDateFilterTimeUnit} timeUnitType - Defines the unit of time the filter is using 35 | * @returns {RelativeTimeFilterBuilder} 36 | */ 37 | inLast(timeUnitsCount: number, timeUnitType: RelativeDateFilterTimeUnit): RelativeTimeFilterBuilder { 38 | this.operator = RelativeDateOperators.InLast; 39 | this.timeUnitsCount = timeUnitsCount; 40 | this.timeUnitType = timeUnitType; 41 | return this; 42 | } 43 | 44 | /** 45 | * Sets inThis as operator for Relative Time filter 46 | * 47 | * ```javascript 48 | * 49 | * const relativeTimeFilterBuilder = new RelativeTimeFilterBuilder().inThis(timeUnitsCount, timeUnitType); 50 | * ``` 51 | * 52 | * @param {number} timeUnitsCount - The amount of time units 53 | * @param {RelativeDateFilterTimeUnit} timeUnitType - Defines the unit of time the filter is using 54 | * @returns {RelativeTimeFilterBuilder} 55 | */ 56 | inThis(timeUnitsCount: number, timeUnitType: RelativeDateFilterTimeUnit): RelativeTimeFilterBuilder { 57 | this.operator = RelativeDateOperators.InThis; 58 | this.timeUnitsCount = timeUnitsCount; 59 | this.timeUnitType = timeUnitType; 60 | return this; 61 | } 62 | 63 | /** 64 | * Sets inNext as operator for Relative Time filter 65 | * 66 | * ```javascript 67 | * 68 | * const relativeTimeFilterBuilder = new RelativeTimeFilterBuilder().inNext(timeUnitsCount, timeUnitType); 69 | * ``` 70 | * 71 | * @param {number} timeUnitsCount - The amount of time units 72 | * @param {RelativeDateFilterTimeUnit} timeUnitType - Defines the unit of time the filter is using 73 | * @returns {RelativeTimeFilterBuilder} 74 | */ 75 | inNext(timeUnitsCount: number, timeUnitType: RelativeDateFilterTimeUnit): RelativeTimeFilterBuilder { 76 | this.operator = RelativeDateOperators.InNext; 77 | this.timeUnitsCount = timeUnitsCount; 78 | this.timeUnitType = timeUnitType; 79 | return this; 80 | } 81 | 82 | /** 83 | * Creates Relative Time filter 84 | * 85 | * ```javascript 86 | * 87 | * const relativeTimeFilterBuilder = new RelativeTimeFilterBuilder().build(); 88 | * ``` 89 | * 90 | * @returns {RelativeTimeFilter} 91 | */ 92 | build(): RelativeTimeFilter { 93 | const relativeTimeFilter = new RelativeTimeFilter(this.target, this.operator, this.timeUnitsCount, this.timeUnitType); 94 | return relativeTimeFilter; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/FilterBuilders/topNFilterBuilder.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | import { 5 | ITarget, 6 | TopNFilter, 7 | TopNFilterOperators 8 | } from "powerbi-models"; 9 | 10 | import { FilterBuilder } from './filterBuilder'; 11 | 12 | /** 13 | * Power BI Top N filter builder component 14 | * 15 | * @export 16 | * @class TopNFilterBuilder 17 | * @extends {FilterBuilder} 18 | */ 19 | export class TopNFilterBuilder extends FilterBuilder { 20 | 21 | private itemCount: number; 22 | private operator: TopNFilterOperators; 23 | private orderByTargetValue: ITarget; 24 | 25 | /** 26 | * Sets Top as operator for Top N filter 27 | * 28 | * ```javascript 29 | * 30 | * const topNFilterBuilder = new TopNFilterBuilder().top(itemCount); 31 | * ``` 32 | * 33 | * @returns {TopNFilterBuilder} 34 | */ 35 | top(itemCount: number): TopNFilterBuilder { 36 | this.operator = "Top"; 37 | this.itemCount = itemCount; 38 | return this; 39 | } 40 | 41 | /** 42 | * Sets Bottom as operator for Top N filter 43 | * 44 | * ```javascript 45 | * 46 | * const topNFilterBuilder = new TopNFilterBuilder().bottom(itemCount); 47 | * ``` 48 | * 49 | * @returns {TopNFilterBuilder} 50 | */ 51 | bottom(itemCount: number): TopNFilterBuilder { 52 | this.operator = "Bottom"; 53 | this.itemCount = itemCount; 54 | return this; 55 | } 56 | 57 | /** 58 | * Sets order by for Top N filter 59 | * 60 | * ```javascript 61 | * 62 | * const topNFilterBuilder = new TopNFilterBuilder().orderByTarget(target); 63 | * ``` 64 | * 65 | * @returns {TopNFilterBuilder} 66 | */ 67 | orderByTarget(target: ITarget): TopNFilterBuilder { 68 | this.orderByTargetValue = target; 69 | return this; 70 | } 71 | 72 | /** 73 | * Creates Top N filter 74 | * 75 | * ```javascript 76 | * 77 | * const topNFilterBuilder = new TopNFilterBuilder().build(); 78 | * ``` 79 | * 80 | * @returns {TopNFilter} 81 | */ 82 | build(): TopNFilter { 83 | const topNFilter = new TopNFilter(this.target, this.operator, this.itemCount, this.orderByTargetValue); 84 | return topNFilter; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/bookmarksManager.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | import { 5 | BookmarksPlayMode, 6 | IApplyBookmarkByNameRequest, 7 | IApplyBookmarkStateRequest, 8 | ICaptureBookmarkOptions, 9 | ICaptureBookmarkRequest, 10 | IPlayBookmarkRequest, 11 | IReportBookmark 12 | 13 | } from 'powerbi-models'; 14 | import { IHttpPostMessageResponse } from 'http-post-message'; 15 | import { Service } from './service'; 16 | import { IEmbedConfigurationBase } from './embed'; 17 | import { isRDLEmbed } from './util'; 18 | import { APINotSupportedForRDLError } from './errors'; 19 | 20 | /** 21 | * APIs for managing the report bookmarks. 22 | * 23 | * @export 24 | * @interface IBookmarksManager 25 | */ 26 | export interface IBookmarksManager { 27 | getBookmarks(): Promise; 28 | apply(bookmarkName: string): Promise>; 29 | play(playMode: BookmarksPlayMode): Promise>; 30 | capture(options?: ICaptureBookmarkOptions): Promise; 31 | applyState(state: string): Promise>; 32 | } 33 | 34 | /** 35 | * Manages report bookmarks. 36 | * 37 | * @export 38 | * @class BookmarksManager 39 | * @implements {IBookmarksManager} 40 | */ 41 | export class BookmarksManager implements IBookmarksManager { 42 | /** 43 | * @hidden 44 | */ 45 | constructor(private service: Service, private config: IEmbedConfigurationBase, private iframe?: HTMLIFrameElement) { 46 | } 47 | 48 | /** 49 | * Gets bookmarks that are defined in the report. 50 | * 51 | * ```javascript 52 | * // Gets bookmarks that are defined in the report 53 | * bookmarksManager.getBookmarks() 54 | * .then(bookmarks => { 55 | * ... 56 | * }); 57 | * ``` 58 | * 59 | * @returns {Promise} 60 | */ 61 | async getBookmarks(): Promise { 62 | if (isRDLEmbed(this.config.embedUrl)) { 63 | return Promise.reject(APINotSupportedForRDLError); 64 | } 65 | 66 | try { 67 | const response = await this.service.hpm.get(`/report/bookmarks`, { uid: this.config.uniqueId }, this.iframe.contentWindow); 68 | return response.body; 69 | } catch (response) { 70 | throw response.body; 71 | } 72 | } 73 | 74 | /** 75 | * Apply bookmark by name. 76 | * 77 | * ```javascript 78 | * bookmarksManager.apply(bookmarkName) 79 | * ``` 80 | * 81 | * @param {string} bookmarkName The name of the bookmark to be applied 82 | * @returns {Promise>} 83 | */ 84 | async apply(bookmarkName: string): Promise> { 85 | if (isRDLEmbed(this.config.embedUrl)) { 86 | return Promise.reject(APINotSupportedForRDLError); 87 | } 88 | 89 | const request: IApplyBookmarkByNameRequest = { 90 | name: bookmarkName 91 | }; 92 | 93 | try { 94 | return await this.service.hpm.post(`/report/bookmarks/applyByName`, request, { uid: this.config.uniqueId }, this.iframe.contentWindow); 95 | } catch (response) { 96 | throw response.body; 97 | } 98 | } 99 | 100 | /** 101 | * Play bookmarks: Enter or Exit bookmarks presentation mode. 102 | * 103 | * ```javascript 104 | * // Enter presentation mode. 105 | * bookmarksManager.play(BookmarksPlayMode.Presentation) 106 | * ``` 107 | * 108 | * @param {BookmarksPlayMode} playMode Play mode can be either `Presentation` or `Off` 109 | * @returns {Promise>} 110 | */ 111 | async play(playMode: BookmarksPlayMode): Promise> { 112 | if (isRDLEmbed(this.config.embedUrl)) { 113 | return Promise.reject(APINotSupportedForRDLError); 114 | } 115 | 116 | const playBookmarkRequest: IPlayBookmarkRequest = { 117 | playMode: playMode 118 | }; 119 | 120 | try { 121 | return await this.service.hpm.post(`/report/bookmarks/play`, playBookmarkRequest, { uid: this.config.uniqueId }, this.iframe.contentWindow); 122 | } catch (response) { 123 | throw response.body; 124 | } 125 | } 126 | 127 | /** 128 | * Capture bookmark from current state. 129 | * 130 | * ```javascript 131 | * bookmarksManager.capture(options) 132 | * ``` 133 | * 134 | * @param {ICaptureBookmarkOptions} [options] Options for bookmark capturing 135 | * @returns {Promise} 136 | */ 137 | async capture(options?: ICaptureBookmarkOptions): Promise { 138 | if (isRDLEmbed(this.config.embedUrl)) { 139 | return Promise.reject(APINotSupportedForRDLError); 140 | } 141 | 142 | const request: ICaptureBookmarkRequest = { 143 | options: options || {} 144 | }; 145 | 146 | try { 147 | const response = await this.service.hpm.post(`/report/bookmarks/capture`, request, { uid: this.config.uniqueId }, this.iframe.contentWindow); 148 | return response.body; 149 | } catch (response) { 150 | throw response.body; 151 | } 152 | } 153 | 154 | /** 155 | * Apply bookmark state. 156 | * 157 | * ```javascript 158 | * bookmarksManager.applyState(bookmarkState) 159 | * ``` 160 | * 161 | * @param {string} state A base64 bookmark state to be applied 162 | * @returns {Promise>} 163 | */ 164 | async applyState(state: string): Promise> { 165 | if (isRDLEmbed(this.config.embedUrl)) { 166 | return Promise.reject(APINotSupportedForRDLError); 167 | } 168 | 169 | const request: IApplyBookmarkStateRequest = { 170 | state: state 171 | }; 172 | 173 | try { 174 | return await this.service.hpm.post(`/report/bookmarks/applyState`, request, { uid: this.config.uniqueId }, this.iframe.contentWindow); 175 | } catch (response) { 176 | throw response.body; 177 | } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | /** @ignore *//** */ 5 | const config = { 6 | version: '2.23.1', 7 | type: 'js' 8 | }; 9 | 10 | export default config; 11 | -------------------------------------------------------------------------------- /src/create.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | import { IReportCreateConfiguration, IError, validateCreateReport } from 'powerbi-models'; 5 | import { Service } from './service'; 6 | import { Embed, IEmbedConfigurationBase, IEmbedConfiguration, ISessionHeaders } from './embed'; 7 | import * as utils from './util'; 8 | 9 | /** 10 | * A Power BI Report creator component 11 | * 12 | * @export 13 | * @class Create 14 | * @extends {Embed} 15 | */ 16 | export class Create extends Embed { 17 | /** 18 | * Gets or sets the configuration settings for creating report. 19 | * 20 | * @type {IReportCreateConfiguration} 21 | * @hidden 22 | */ 23 | createConfig: IReportCreateConfiguration; 24 | 25 | /* 26 | * @hidden 27 | */ 28 | constructor(service: Service, element: HTMLElement, config: IEmbedConfiguration | IReportCreateConfiguration, phasedRender?: boolean, isBootstrap?: boolean) { 29 | super(service, element, config, /* iframe */ undefined, phasedRender, isBootstrap); 30 | } 31 | 32 | /** 33 | * Gets the dataset ID from the first available location: createConfig or embed url. 34 | * 35 | * @returns {string} 36 | */ 37 | getId(): string { 38 | const datasetId = (this.createConfig && this.createConfig.datasetId) ? this.createConfig.datasetId : Create.findIdFromEmbedUrl(this.config.embedUrl); 39 | 40 | if (typeof datasetId !== 'string' || datasetId.length === 0) { 41 | throw new Error('Dataset id is required, but it was not found. You must provide an id either as part of embed configuration.'); 42 | } 43 | 44 | return datasetId; 45 | } 46 | 47 | /** 48 | * Validate create report configuration. 49 | */ 50 | validate(config: IEmbedConfigurationBase): IError[] { 51 | return validateCreateReport(config); 52 | } 53 | 54 | /** 55 | * Handle config changes. 56 | * 57 | * @hidden 58 | * @returns {void} 59 | */ 60 | configChanged(isBootstrap: boolean): void { 61 | if (isBootstrap) { 62 | return; 63 | } 64 | 65 | const config = this.config as IEmbedConfiguration | IReportCreateConfiguration; 66 | 67 | this.createConfig = { 68 | accessToken: config.accessToken, 69 | datasetId: config.datasetId || this.getId(), 70 | groupId: config.groupId, 71 | settings: config.settings, 72 | tokenType: config.tokenType, 73 | theme: config.theme 74 | }; 75 | } 76 | 77 | /** 78 | * @hidden 79 | * @returns {string} 80 | */ 81 | getDefaultEmbedUrlEndpoint(): string { 82 | return "reportEmbed"; 83 | } 84 | 85 | /** 86 | * checks if the report is saved. 87 | * 88 | * ```javascript 89 | * report.isSaved() 90 | * ``` 91 | * 92 | * @returns {Promise} 93 | */ 94 | async isSaved(): Promise { 95 | return await utils.isSavedInternal(this.service.hpm, this.config.uniqueId, this.iframe.contentWindow); 96 | } 97 | 98 | /** 99 | * Adds the ability to get datasetId from url. 100 | * (e.g. http://embedded.powerbi.com/appTokenReportEmbed?datasetId=854846ed-2106-4dc2-bc58-eb77533bf2f1). 101 | * 102 | * By extracting the ID we can ensure that the ID is always explicitly provided as part of the create configuration. 103 | * 104 | * @static 105 | * @param {string} url 106 | * @returns {string} 107 | * @hidden 108 | */ 109 | static findIdFromEmbedUrl(url: string): string { 110 | const datasetIdRegEx = /datasetId="?([^&]+)"?/; 111 | const datasetIdMatch = url.match(datasetIdRegEx); 112 | 113 | let datasetId: string; 114 | if (datasetIdMatch) { 115 | datasetId = datasetIdMatch[1]; 116 | } 117 | 118 | return datasetId; 119 | } 120 | 121 | /** 122 | * Sends create configuration data. 123 | * 124 | * ```javascript 125 | * create ({ 126 | * datasetId: '5dac7a4a-4452-46b3-99f6-a25915e0fe55', 127 | * accessToken: 'eyJ0eXA ... TaE2rTSbmg', 128 | * ``` 129 | * 130 | * @hidden 131 | * @returns {Promise} 132 | */ 133 | async create(): Promise { 134 | const errors = validateCreateReport(this.createConfig); 135 | if (errors) { 136 | throw errors; 137 | } 138 | 139 | try { 140 | const headers: ISessionHeaders = { 141 | uid: this.config.uniqueId, 142 | sdkSessionId: this.service.getSdkSessionId() 143 | }; 144 | 145 | if (!!this.eventHooks?.accessTokenProvider) { 146 | headers.tokenProviderSupplied = true; 147 | } 148 | 149 | const response = await this.service.hpm.post("/report/create", this.createConfig, headers, this.iframe.contentWindow); 150 | return response.body; 151 | } catch (response) { 152 | throw response.body; 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/dashboard.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | import { IError, validateDashboardLoad, PageView } from 'powerbi-models'; 5 | import { Service, IService } from './service'; 6 | import { Embed, IDashboardEmbedConfiguration, IEmbedConfigurationBase } from './embed'; 7 | 8 | /** 9 | * A Dashboard node within a dashboard hierarchy 10 | * 11 | * @export 12 | * @interface IDashboardNode 13 | */ 14 | export interface IDashboardNode { 15 | iframe: HTMLIFrameElement; 16 | service: IService; 17 | config: IEmbedConfigurationBase; 18 | } 19 | 20 | /** 21 | * A Power BI Dashboard embed component 22 | * 23 | * @export 24 | * @class Dashboard 25 | * @extends {Embed} 26 | * @implements {IDashboardNode} 27 | */ 28 | export class Dashboard extends Embed implements IDashboardNode { 29 | /** @hidden */ 30 | static allowedEvents = ["tileClicked", "error"]; 31 | /** @hidden */ 32 | static dashboardIdAttribute = 'powerbi-dashboard-id'; 33 | /** @hidden */ 34 | static typeAttribute = 'powerbi-type'; 35 | /** @hidden */ 36 | static type = "Dashboard"; 37 | 38 | /** 39 | * Creates an instance of a Power BI Dashboard. 40 | * 41 | * @param {service.Service} service 42 | * @hidden 43 | * @param {HTMLElement} element 44 | */ 45 | constructor(service: Service, element: HTMLElement, config: IEmbedConfigurationBase, phasedRender?: boolean, isBootstrap?: boolean) { 46 | super(service, element, config, /* iframe */ undefined, phasedRender, isBootstrap); 47 | this.loadPath = "/dashboard/load"; 48 | this.phasedLoadPath = "/dashboard/prepare"; 49 | 50 | Array.prototype.push.apply(this.allowedEvents, Dashboard.allowedEvents); 51 | } 52 | 53 | /** 54 | * This adds backwards compatibility for older config which used the dashboardId query param to specify dashboard id. 55 | * E.g. https://powerbi-df.analysis-df.windows.net/dashboardEmbedHost?dashboardId=e9363c62-edb6-4eac-92d3-2199c5ca2a9e 56 | * 57 | * By extracting the id we can ensure id is always explicitly provided as part of the load configuration. 58 | * 59 | * @hidden 60 | * @static 61 | * @param {string} url 62 | * @returns {string} 63 | */ 64 | static findIdFromEmbedUrl(url: string): string { 65 | const dashboardIdRegEx = /dashboardId="?([^&]+)"?/; 66 | const dashboardIdMatch = url.match(dashboardIdRegEx); 67 | 68 | let dashboardId: string; 69 | if (dashboardIdMatch) { 70 | dashboardId = dashboardIdMatch[1]; 71 | } 72 | 73 | return dashboardId; 74 | } 75 | 76 | /** 77 | * Get dashboard id from first available location: options, attribute, embed url. 78 | * 79 | * @returns {string} 80 | */ 81 | getId(): string { 82 | const config = this.config as IDashboardEmbedConfiguration; 83 | const dashboardId = config.id || this.element.getAttribute(Dashboard.dashboardIdAttribute) || Dashboard.findIdFromEmbedUrl(config.embedUrl); 84 | 85 | if (typeof dashboardId !== 'string' || dashboardId.length === 0) { 86 | throw new Error(`Dashboard id is required, but it was not found. You must provide an id either as part of embed configuration or as attribute '${Dashboard.dashboardIdAttribute}'.`); 87 | } 88 | 89 | return dashboardId; 90 | } 91 | 92 | /** 93 | * Validate load configuration. 94 | * 95 | * @hidden 96 | */ 97 | validate(baseConfig: IEmbedConfigurationBase): IError[] { 98 | const config = baseConfig as IDashboardEmbedConfiguration; 99 | const error = validateDashboardLoad(config); 100 | return error ? error : this.validatePageView(config.pageView); 101 | } 102 | 103 | /** 104 | * Handle config changes. 105 | * 106 | * @hidden 107 | * @returns {void} 108 | */ 109 | configChanged(isBootstrap: boolean): void { 110 | if (isBootstrap) { 111 | return; 112 | } 113 | 114 | // Populate dashboard id into config object. 115 | (this.config as IDashboardEmbedConfiguration).id = this.getId(); 116 | } 117 | 118 | /** 119 | * @hidden 120 | * @returns {string} 121 | */ 122 | getDefaultEmbedUrlEndpoint(): string { 123 | return "dashboardEmbed"; 124 | } 125 | 126 | /** 127 | * Validate that pageView has a legal value: if page view is defined it must have one of the values defined in PageView 128 | * 129 | * @hidden 130 | */ 131 | private validatePageView(pageView: PageView): IError[] { 132 | if (pageView && pageView !== "fitToWidth" && pageView !== "oneColumn" && pageView !== "actualSize") { 133 | return [{ message: "pageView must be one of the followings: fitToWidth, oneColumn, actualSize" }]; 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/errors.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | export const APINotSupportedForRDLError = "This API is currently not supported for RDL reports"; 5 | export const EmbedUrlNotSupported = "Embed URL is invalid for this scenario. Please use Power BI REST APIs to get the valid URL"; 6 | export const invalidEmbedUrlErrorMessage: string = "Invalid embed URL detected. Either URL hostname or protocol are invalid. Please use Power BI REST APIs to get the valid URL"; 7 | 8 | -------------------------------------------------------------------------------- /src/factories.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | /** 5 | * TODO: Need to find better place for these factory functions or refactor how we handle dependency injection 6 | */ 7 | import { WindowPostMessageProxy } from 'window-post-message-proxy'; 8 | import { HttpPostMessage } from 'http-post-message'; 9 | import { Router } from 'powerbi-router'; 10 | import config from './config'; 11 | import { IHpmFactory, IWpmpFactory, IRouterFactory } from './service'; 12 | 13 | export { 14 | IHpmFactory, 15 | IWpmpFactory, 16 | IRouterFactory 17 | }; 18 | 19 | export const hpmFactory: IHpmFactory = (wpmp, defaultTargetWindow, sdkVersion = config.version, sdkType = config.type, sdkWrapperVersion?: string) => { 20 | return new HttpPostMessage(wpmp, { 21 | 'x-sdk-type': sdkType, 22 | 'x-sdk-version': sdkVersion, 23 | 'x-sdk-wrapper-version': sdkWrapperVersion, 24 | }, defaultTargetWindow); 25 | }; 26 | 27 | export const wpmpFactory: IWpmpFactory = (name?: string, logMessages?: boolean, eventSourceOverrideWindow?: Window) => { 28 | return new WindowPostMessageProxy({ 29 | processTrackingProperties: { 30 | addTrackingProperties: HttpPostMessage.addTrackingProperties, 31 | getTrackingProperties: HttpPostMessage.getTrackingProperties, 32 | }, 33 | isErrorMessage: HttpPostMessage.isErrorMessage, 34 | suppressWarnings: true, 35 | name: name, 36 | logMessages: logMessages, 37 | eventSourceOverrideWindow: eventSourceOverrideWindow 38 | }); 39 | }; 40 | 41 | export const routerFactory: IRouterFactory = (wpmp) => { 42 | return new Router(wpmp); 43 | }; 44 | -------------------------------------------------------------------------------- /src/ifilterable.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | import { FiltersOperations, IFilter } from 'powerbi-models'; 5 | import { IHttpPostMessageResponse } from 'http-post-message'; 6 | 7 | /** 8 | * Decorates embed components that support filters 9 | * Examples include reports and pages 10 | * 11 | * @export 12 | * @interface IFilterable 13 | */ 14 | export interface IFilterable { 15 | /** 16 | * Gets the filters currently applied to the object. 17 | * 18 | * @returns {(Promise)} 19 | */ 20 | getFilters(): Promise; 21 | /** 22 | * Update the filters for the current instance according to the operation: Add, replace all, replace by target or remove. 23 | * 24 | * @param {(FiltersOperations)} operation 25 | * @param {(IFilter[])} filters 26 | * @returns {Promise>} 27 | */ 28 | updateFilters(operation: FiltersOperations, filters?: IFilter[]): Promise>; 29 | /** 30 | * Removes all filters from the current object. 31 | * 32 | * @returns {Promise>} 33 | */ 34 | removeFilters(): Promise>; 35 | /** 36 | * Replaces all filters on the current object with the specified filter values. 37 | * 38 | * @param {(IFilter[])} filters 39 | * @returns {Promise>} 40 | */ 41 | setFilters(filters: IFilter[]): Promise>; 42 | } 43 | -------------------------------------------------------------------------------- /src/page.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | import { IHttpPostMessageResponse } from 'http-post-message'; 5 | import { 6 | CommonErrorCodes, 7 | DisplayOption, 8 | FiltersOperations, 9 | ICustomPageSize, 10 | IFilter, 11 | IPage, 12 | IUpdateFiltersRequest, 13 | IVisual, 14 | LayoutType, 15 | PageLevelFilters, 16 | PageSizeType, 17 | SectionVisibility, 18 | VisualContainerDisplayMode, 19 | IPageBackground, 20 | IPageWallpaper, 21 | ISmartNarratives, 22 | } from 'powerbi-models'; 23 | import { IFilterable } from './ifilterable'; 24 | import { IReportNode, Report } from './report'; 25 | import { VisualDescriptor } from './visualDescriptor'; 26 | import { isRDLEmbed } from './util'; 27 | import { APINotSupportedForRDLError } from './errors'; 28 | 29 | /** 30 | * A Page node within a report hierarchy 31 | * 32 | * @export 33 | * @interface IPageNode 34 | */ 35 | export interface IPageNode { 36 | report: IReportNode; 37 | name: string; 38 | } 39 | 40 | /** 41 | * A Power BI report page 42 | * 43 | * @export 44 | * @class Page 45 | * @implements {IPageNode} 46 | * @implements {IFilterable} 47 | */ 48 | export class Page implements IPageNode, IFilterable { 49 | /** 50 | * The parent Power BI report that this page is a member of 51 | * 52 | * @type {IReportNode} 53 | */ 54 | report: IReportNode; 55 | /** 56 | * The report page name 57 | * 58 | * @type {string} 59 | */ 60 | name: string; 61 | 62 | /** 63 | * The user defined display name of the report page, which is undefined if the page is created manually 64 | * 65 | * @type {string} 66 | */ 67 | displayName: string; 68 | 69 | /** 70 | * Is this page is the active page 71 | * 72 | * @type {boolean} 73 | */ 74 | isActive: boolean; 75 | 76 | /** 77 | * The visibility of the page. 78 | * 0 - Always Visible 79 | * 1 - Hidden in View Mode 80 | * 81 | * @type {SectionVisibility} 82 | */ 83 | visibility: SectionVisibility; 84 | 85 | /** 86 | * Page size as saved in the report. 87 | * 88 | * @type {ICustomPageSize} 89 | */ 90 | defaultSize: ICustomPageSize; 91 | 92 | /** 93 | * Mobile view page size (if defined) as saved in the report. 94 | * 95 | * @type {ICustomPageSize} 96 | */ 97 | mobileSize: ICustomPageSize; 98 | 99 | /** 100 | * Page display options as saved in the report. 101 | * 102 | * @type {ICustomPageSize} 103 | */ 104 | defaultDisplayOption: DisplayOption; 105 | 106 | /** 107 | * Page background color. 108 | * 109 | * @type {IPageBackground} 110 | */ 111 | background: IPageBackground; 112 | 113 | /** 114 | * Page wallpaper color. 115 | * 116 | * @type {IPageWallpaper} 117 | */ 118 | wallpaper: IPageWallpaper; 119 | 120 | /** 121 | * Creates an instance of a Power BI report page. 122 | * 123 | * @param {IReportNode} report 124 | * @param {string} name 125 | * @param {string} [displayName] 126 | * @param {boolean} [isActivePage] 127 | * @param {SectionVisibility} [visibility] 128 | * @hidden 129 | */ 130 | constructor(report: IReportNode, name: string, displayName?: string, isActivePage?: boolean, visibility?: SectionVisibility, defaultSize?: ICustomPageSize, defaultDisplayOption?: DisplayOption, mobileSize?: ICustomPageSize, background?: IPageBackground, wallpaper?: IPageWallpaper) { 131 | this.report = report; 132 | this.name = name; 133 | this.displayName = displayName; 134 | this.isActive = isActivePage; 135 | this.visibility = visibility; 136 | this.defaultSize = defaultSize; 137 | this.mobileSize = mobileSize; 138 | this.defaultDisplayOption = defaultDisplayOption; 139 | this.background = background; 140 | this.wallpaper = wallpaper; 141 | } 142 | 143 | /** 144 | * Get insights for report page 145 | * 146 | * ```javascript 147 | * page.getSmartNarrativeInsights(); 148 | * ``` 149 | * 150 | * @returns {Promise} 151 | */ 152 | async getSmartNarrativeInsights(): Promise { 153 | if (isRDLEmbed(this.report.config.embedUrl)) { 154 | return Promise.reject(APINotSupportedForRDLError); 155 | } 156 | 157 | try { 158 | const response = await this.report.service.hpm.get(`/report/pages/${this.name}/smartNarrativeInsights`, { uid: this.report.config.uniqueId }, this.report.iframe.contentWindow); 159 | return response.body; 160 | } catch (response) { 161 | throw response.body; 162 | } 163 | } 164 | 165 | /** 166 | * Gets all page level filters within the report. 167 | * 168 | * ```javascript 169 | * page.getFilters() 170 | * .then(filters => { ... }); 171 | * ``` 172 | * 173 | * @returns {(Promise)} 174 | */ 175 | async getFilters(): Promise { 176 | try { 177 | const response = await this.report.service.hpm.get(`/report/pages/${this.name}/filters`, { uid: this.report.config.uniqueId }, this.report.iframe.contentWindow); 178 | return response.body; 179 | } catch (response) { 180 | throw response.body; 181 | } 182 | } 183 | 184 | /** 185 | * Update the filters for the current page according to the operation: Add, replace all, replace by target or remove. 186 | * 187 | * ```javascript 188 | * page.updateFilters(FiltersOperations.Add, filters) 189 | * .catch(errors => { ... }); 190 | * ``` 191 | * 192 | * @param {(IFilter[])} filters 193 | * @returns {Promise>} 194 | */ 195 | async updateFilters(operation: FiltersOperations, filters?: IFilter[]): Promise> { 196 | const updateFiltersRequest: IUpdateFiltersRequest = { 197 | filtersOperation: operation, 198 | filters: filters as PageLevelFilters[] 199 | }; 200 | 201 | try { 202 | return await this.report.service.hpm.post(`/report/pages/${this.name}/filters`, updateFiltersRequest, { uid: this.report.config.uniqueId }, this.report.iframe.contentWindow); 203 | } catch (response) { 204 | throw response.body; 205 | } 206 | } 207 | 208 | /** 209 | * Removes all filters from this page of the report. 210 | * 211 | * ```javascript 212 | * page.removeFilters(); 213 | * ``` 214 | * 215 | * @returns {Promise>} 216 | */ 217 | async removeFilters(): Promise> { 218 | return await this.updateFilters(FiltersOperations.RemoveAll); 219 | } 220 | 221 | /** 222 | * Sets all filters on the current page. 223 | * 224 | * ```javascript 225 | * page.setFilters(filters) 226 | * .catch(errors => { ... }); 227 | * ``` 228 | * 229 | * @param {(IFilter[])} filters 230 | * @returns {Promise>} 231 | */ 232 | async setFilters(filters: IFilter[]): Promise> { 233 | try { 234 | return await this.report.service.hpm.put(`/report/pages/${this.name}/filters`, filters, { uid: this.report.config.uniqueId }, this.report.iframe.contentWindow); 235 | } catch (response) { 236 | throw response.body; 237 | } 238 | } 239 | 240 | /** 241 | * Delete the page from the report 242 | * 243 | * ```javascript 244 | * // Delete the page from the report 245 | * page.delete(); 246 | * ``` 247 | * 248 | * @returns {Promise} 249 | */ 250 | async delete(): Promise { 251 | try { 252 | const response = await this.report.service.hpm.delete(`/report/pages/${this.name}`, {}, { uid: this.report.config.uniqueId }, this.report.iframe.contentWindow); 253 | return response.body; 254 | } catch (response) { 255 | throw response.body; 256 | } 257 | } 258 | 259 | /** 260 | * Makes the current page the active page of the report. 261 | * 262 | * ```javascript 263 | * page.setActive(); 264 | * ``` 265 | * 266 | * @returns {Promise>} 267 | */ 268 | async setActive(): Promise> { 269 | const page: IPage = { 270 | name: this.name, 271 | displayName: null, 272 | isActive: true 273 | }; 274 | 275 | try { 276 | return await this.report.service.hpm.put('/report/pages/active', page, { uid: this.report.config.uniqueId }, this.report.iframe.contentWindow); 277 | } catch (response) { 278 | throw response.body; 279 | } 280 | } 281 | 282 | /** 283 | * Set displayName to the current page. 284 | * 285 | * ```javascript 286 | * page.setName(displayName); 287 | * ``` 288 | * 289 | * @returns {Promise>} 290 | */ 291 | async setDisplayName(displayName: string): Promise> { 292 | const page: IPage = { 293 | name: this.name, 294 | displayName: displayName, 295 | }; 296 | 297 | try { 298 | return await this.report.service.hpm.put(`/report/pages/${this.name}/name`, page, { uid: this.report.config.uniqueId }, this.report.iframe.contentWindow); 299 | } catch (response) { 300 | throw response.body; 301 | } 302 | } 303 | 304 | /** 305 | * Gets all the visuals on the page. 306 | * 307 | * ```javascript 308 | * page.getVisuals() 309 | * .then(visuals => { ... }); 310 | * ``` 311 | * 312 | * @returns {Promise} 313 | */ 314 | async getVisuals(): Promise { 315 | if (isRDLEmbed(this.report.config.embedUrl)) { 316 | return Promise.reject(APINotSupportedForRDLError); 317 | } 318 | 319 | try { 320 | const response = await this.report.service.hpm.get(`/report/pages/${this.name}/visuals`, { uid: this.report.config.uniqueId }, this.report.iframe.contentWindow); 321 | return response.body 322 | .map((visual) => new VisualDescriptor(this, visual.name, visual.title, visual.type, visual.layout)); 323 | } catch (response) { 324 | throw response.body; 325 | } 326 | } 327 | 328 | /** 329 | * Gets a visual by name on the page. 330 | * 331 | * ```javascript 332 | * page.getVisualByName(visualName: string) 333 | * .then(visual => { 334 | * ... 335 | * }); 336 | * ``` 337 | * 338 | * @param {string} visualName 339 | * @returns {Promise} 340 | */ 341 | async getVisualByName(visualName: string): Promise { 342 | if (isRDLEmbed(this.report.config.embedUrl)) { 343 | return Promise.reject(APINotSupportedForRDLError); 344 | } 345 | 346 | try { 347 | const response = await this.report.service.hpm.get(`/report/pages/${this.name}/visuals`, { uid: this.report.config.uniqueId }, this.report.iframe.contentWindow); 348 | const visual = response.body.find((v: IVisual) => v.name === visualName); 349 | if (!visual) { 350 | return Promise.reject(CommonErrorCodes.NotFound); 351 | } 352 | 353 | return new VisualDescriptor(this, visual.name, visual.title, visual.type, visual.layout); 354 | } catch (response) { 355 | throw response.body; 356 | } 357 | } 358 | 359 | /** 360 | * Updates the display state of a visual in a page. 361 | * 362 | * ```javascript 363 | * page.setVisualDisplayState(visualName, displayState) 364 | * .catch(error => { ... }); 365 | * ``` 366 | * 367 | * @param {string} visualName 368 | * @param {VisualContainerDisplayMode} displayState 369 | * @returns {Promise>} 370 | */ 371 | async setVisualDisplayState(visualName: string, displayState: VisualContainerDisplayMode): Promise> { 372 | const pageName = this.name; 373 | const report = this.report as Report; 374 | return report.setVisualDisplayState(pageName, visualName, displayState); 375 | } 376 | 377 | /** 378 | * Updates the position of a visual in a page. 379 | * 380 | * ```javascript 381 | * page.moveVisual(visualName, x, y, z) 382 | * .catch(error => { ... }); 383 | * ``` 384 | * 385 | * @param {string} visualName 386 | * @param {number} x 387 | * @param {number} y 388 | * @param {number} z 389 | * @returns {Promise>} 390 | */ 391 | async moveVisual(visualName: string, x: number, y: number, z?: number): Promise> { 392 | const pageName = this.name; 393 | const report = this.report as Report; 394 | return report.moveVisual(pageName, visualName, x, y, z); 395 | } 396 | 397 | /** 398 | * Resize a visual in a page. 399 | * 400 | * ```javascript 401 | * page.resizeVisual(visualName, width, height) 402 | * .catch(error => { ... }); 403 | * ``` 404 | * 405 | * @param {string} visualName 406 | * @param {number} width 407 | * @param {number} height 408 | * @returns {Promise>} 409 | */ 410 | async resizeVisual(visualName: string, width: number, height: number): Promise> { 411 | const pageName = this.name; 412 | const report = this.report as Report; 413 | return report.resizeVisual(pageName, visualName, width, height); 414 | } 415 | 416 | /** 417 | * Updates the size of active page. 418 | * 419 | * ```javascript 420 | * page.resizePage(pageSizeType, width, height) 421 | * .catch(error => { ... }); 422 | * ``` 423 | * 424 | * @param {PageSizeType} pageSizeType 425 | * @param {number} width 426 | * @param {number} height 427 | * @returns {Promise>} 428 | */ 429 | async resizePage(pageSizeType: PageSizeType, width?: number, height?: number): Promise> { 430 | if (!this.isActive) { 431 | return Promise.reject('Cannot resize the page. Only the active page can be resized'); 432 | } 433 | const report = this.report as Report; 434 | return report.resizeActivePage(pageSizeType, width, height); 435 | } 436 | 437 | /** 438 | * Gets the list of slicer visuals on the page. 439 | * 440 | * ```javascript 441 | * page.getSlicers() 442 | * .then(slicers => { 443 | * ... 444 | * }); 445 | * ``` 446 | * 447 | * @returns {Promise} 448 | */ 449 | async getSlicers(): Promise { 450 | if (isRDLEmbed(this.report.config.embedUrl)) { 451 | return Promise.reject(APINotSupportedForRDLError); 452 | } 453 | 454 | try { 455 | const response = await this.report.service.hpm.get(`/report/pages/${this.name}/visuals`, { uid: this.report.config.uniqueId }, this.report.iframe.contentWindow); 456 | return response.body 457 | .filter((visual: IVisual) => visual.type === 'slicer') 458 | .map((visual: IVisual) => new VisualDescriptor(this, visual.name, visual.title, visual.type, visual.layout)); 459 | } catch (response) { 460 | throw response.body; 461 | } 462 | } 463 | 464 | /** 465 | * Checks if page has layout. 466 | * 467 | * ```javascript 468 | * page.hasLayout(layoutType) 469 | * .then(hasLayout: boolean => { ... }); 470 | * ``` 471 | * 472 | * @returns {(Promise)} 473 | */ 474 | async hasLayout(layoutType: LayoutType): Promise { 475 | if (isRDLEmbed(this.report.config.embedUrl)) { 476 | return Promise.reject(APINotSupportedForRDLError); 477 | } 478 | 479 | const layoutTypeEnum = LayoutType[layoutType]; 480 | try { 481 | const response = await this.report.service.hpm.get(`/report/pages/${this.name}/layoutTypes/${layoutTypeEnum}`, { uid: this.report.config.uniqueId }, this.report.iframe.contentWindow); 482 | return response.body; 483 | } catch (response) { 484 | throw response.body; 485 | } 486 | } 487 | } 488 | -------------------------------------------------------------------------------- /src/powerbi-client.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | /** 5 | * @hidden 6 | */ 7 | import * as models from 'powerbi-models'; 8 | import * as service from './service'; 9 | import * as factories from './factories'; 10 | import { IFilterable } from './ifilterable'; 11 | 12 | export { 13 | IFilterable, 14 | service, 15 | factories, 16 | models 17 | }; 18 | export { 19 | Report 20 | } from './report'; 21 | export { 22 | Dashboard 23 | } from './dashboard'; 24 | export { 25 | Tile 26 | } from './tile'; 27 | export { 28 | IEmbedConfiguration, 29 | IQnaEmbedConfiguration, 30 | IVisualEmbedConfiguration, 31 | IReportEmbedConfiguration, 32 | IDashboardEmbedConfiguration, 33 | ITileEmbedConfiguration, 34 | IQuickCreateConfiguration, 35 | IReportCreateConfiguration, 36 | Embed, 37 | ILocaleSettings, 38 | IEmbedSettings, 39 | IQnaSettings, 40 | } from './embed'; 41 | export { 42 | Page 43 | } from './page'; 44 | export { 45 | Qna 46 | } from './qna'; 47 | export { 48 | Visual 49 | } from './visual'; 50 | export { 51 | VisualDescriptor 52 | } from './visualDescriptor'; 53 | export { 54 | QuickCreate 55 | } from './quickCreate'; 56 | export { 57 | Create 58 | } from './create'; 59 | export { 60 | BasicFilterBuilder, 61 | AdvancedFilterBuilder, 62 | TopNFilterBuilder, 63 | RelativeDateFilterBuilder, 64 | RelativeTimeFilterBuilder 65 | } from './FilterBuilders'; 66 | 67 | declare var powerbi: service.Service; 68 | declare global { 69 | interface Window { 70 | powerbi: service.Service; 71 | powerBISDKGlobalServiceInstanceName?: string; 72 | } 73 | } 74 | 75 | /** 76 | * Makes Power BI available to the global object for use in applications that don't have module loading support. 77 | * 78 | * Note: create an instance of the class with the default configuration for normal usage, or save the class so that you can create an instance of the service. 79 | */ 80 | var powerbi = new service.Service(factories.hpmFactory, factories.wpmpFactory, factories.routerFactory); 81 | // powerBI SDK may use Power BI object under different key, in order to avoid name collisions 82 | if (window.powerbi && window.powerBISDKGlobalServiceInstanceName) { 83 | window[window.powerBISDKGlobalServiceInstanceName] = powerbi; 84 | } else { 85 | // Default to Power BI. 86 | window.powerbi = powerbi; 87 | } 88 | -------------------------------------------------------------------------------- /src/qna.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | import { IHttpPostMessageResponse } from 'http-post-message'; 5 | import { IError, IQnaInterpretInputData, validateLoadQnaConfiguration } from 'powerbi-models'; 6 | import { Embed, IEmbedConfigurationBase } from './embed'; 7 | import { Service } from './service'; 8 | 9 | /** 10 | * The Power BI Q&A embed component 11 | * 12 | * @export 13 | * @class Qna 14 | * @extends {Embed} 15 | */ 16 | export class Qna extends Embed { 17 | /** @hidden */ 18 | static type = "Qna"; 19 | /** @hidden */ 20 | static allowedEvents = ["loaded", "visualRendered"]; 21 | 22 | /** 23 | * @hidden 24 | */ 25 | constructor(service: Service, element: HTMLElement, config: IEmbedConfigurationBase, phasedRender?: boolean, isBootstrap?: boolean) { 26 | super(service, element, config, /* iframe */ undefined, phasedRender, isBootstrap); 27 | 28 | this.loadPath = "/qna/load"; 29 | this.phasedLoadPath = "/qna/prepare"; 30 | Array.prototype.push.apply(this.allowedEvents, Qna.allowedEvents); 31 | } 32 | 33 | /** 34 | * The ID of the Q&A embed component 35 | * 36 | * @returns {string} 37 | */ 38 | getId(): string { 39 | return null; 40 | } 41 | 42 | /** 43 | * Change the question of the Q&A embed component 44 | * 45 | * @param {string} question - question which will render Q&A data 46 | * @returns {Promise>} 47 | */ 48 | async setQuestion(question: string): Promise> { 49 | const qnaData: IQnaInterpretInputData = { 50 | question: question 51 | }; 52 | 53 | try { 54 | return await this.service.hpm.post('/qna/interpret', qnaData, { uid: this.config.uniqueId }, this.iframe.contentWindow); 55 | } catch (response) { 56 | throw response.body; 57 | } 58 | } 59 | 60 | /** 61 | * Handle config changes. 62 | * 63 | * @returns {void} 64 | */ 65 | configChanged(_isBootstrap: boolean): void { 66 | // Nothing to do in Q&A embed. 67 | } 68 | 69 | /** 70 | * @hidden 71 | * @returns {string} 72 | */ 73 | getDefaultEmbedUrlEndpoint(): string { 74 | return "qnaEmbed"; 75 | } 76 | 77 | /** 78 | * Validate load configuration. 79 | */ 80 | validate(config: IEmbedConfigurationBase): IError[] { 81 | return validateLoadQnaConfiguration(config); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/quickCreate.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | import { IError, IQuickCreateConfiguration, validateQuickCreate } from 'powerbi-models'; 5 | import { Service } from './service'; 6 | import { Embed, IEmbedConfigurationBase, ISessionHeaders } from './embed'; 7 | 8 | /** 9 | * A Power BI Quick Create component 10 | * 11 | * @export 12 | * @class QuickCreate 13 | * @extends {Embed} 14 | */ 15 | export class QuickCreate extends Embed { 16 | 17 | /** 18 | * Gets or sets the configuration settings for creating report. 19 | * 20 | * @type {IQuickCreateConfiguration} 21 | * @hidden 22 | */ 23 | createConfig: IQuickCreateConfiguration; 24 | 25 | /* 26 | * @hidden 27 | */ 28 | constructor(service: Service, element: HTMLElement, config: IQuickCreateConfiguration, phasedRender?: boolean, isBootstrap?: boolean) { 29 | super(service, element, config, /* iframe */ undefined, phasedRender, isBootstrap); 30 | 31 | service.router.post(`/reports/${this.config.uniqueId}/eventHooks/:eventName`, async (req, _res) => { 32 | switch (req.params.eventName) { 33 | case "newAccessToken": 34 | req.body = req.body || {}; 35 | req.body.report = this; 36 | await service.invokeSDKHook(this.eventHooks?.accessTokenProvider, req, _res); 37 | break; 38 | 39 | default: 40 | break; 41 | } 42 | }); 43 | } 44 | 45 | /** 46 | * Override the getId abstract function 47 | * QuickCreate does not need any ID 48 | * 49 | * @returns {string} 50 | */ 51 | getId(): string { 52 | return null; 53 | } 54 | 55 | /** 56 | * Validate create report configuration. 57 | */ 58 | validate(config: IEmbedConfigurationBase): IError[] { 59 | return validateQuickCreate(config); 60 | } 61 | 62 | /** 63 | * Handle config changes. 64 | * 65 | * @hidden 66 | * @returns {void} 67 | */ 68 | configChanged(isBootstrap: boolean): void { 69 | if (isBootstrap) { 70 | return; 71 | } 72 | 73 | this.createConfig = this.config as IQuickCreateConfiguration; 74 | } 75 | 76 | /** 77 | * @hidden 78 | * @returns {string} 79 | */ 80 | getDefaultEmbedUrlEndpoint(): string { 81 | return "quickCreate"; 82 | } 83 | 84 | /** 85 | * Sends quickCreate configuration data. 86 | * 87 | * ```javascript 88 | * quickCreate({ 89 | * accessToken: 'eyJ0eXA ... TaE2rTSbmg', 90 | * datasetCreateConfig: {}}) 91 | * ``` 92 | * 93 | * @hidden 94 | * @param {IQuickCreateConfiguration} createConfig 95 | * @returns {Promise} 96 | */ 97 | async create(): Promise { 98 | const errors = validateQuickCreate(this.createConfig); 99 | if (errors) { 100 | throw errors; 101 | } 102 | 103 | try { 104 | const headers: ISessionHeaders = { 105 | uid: this.config.uniqueId, 106 | sdkSessionId: this.service.getSdkSessionId() 107 | }; 108 | 109 | if (!!this.eventHooks?.accessTokenProvider) { 110 | headers.tokenProviderSupplied = true; 111 | } 112 | 113 | const response = await this.service.hpm.post("/quickcreate", this.createConfig, headers, this.iframe.contentWindow); 114 | return response.body; 115 | } catch (response) { 116 | throw response.body; 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/service.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | /* eslint-disable @typescript-eslint/prefer-function-type */ 5 | /* eslint-disable @typescript-eslint/no-unused-vars */ 6 | /* eslint-disable @typescript-eslint/no-unsafe-assignment */ 7 | import { WindowPostMessageProxy } from 'window-post-message-proxy'; 8 | import { HttpPostMessage } from 'http-post-message'; 9 | import { Router, IExtendedRequest, Response as IExtendedResponse } from 'powerbi-router'; 10 | import { IPage, IQuickCreateConfiguration, IReportCreateConfiguration } from 'powerbi-models'; 11 | import { 12 | Embed, 13 | IBootstrapEmbedConfiguration, 14 | IDashboardEmbedConfiguration, 15 | IEmbedConfiguration, 16 | IEmbedConfigurationBase, 17 | IQnaEmbedConfiguration, 18 | IReportEmbedConfiguration, 19 | ITileEmbedConfiguration, 20 | IVisualEmbedConfiguration, 21 | } from './embed'; 22 | import { Report } from './report'; 23 | import { Create } from './create'; 24 | import { Dashboard } from './dashboard'; 25 | import { Tile } from './tile'; 26 | import { Page } from './page'; 27 | import { Qna } from './qna'; 28 | import { Visual } from './visual'; 29 | import * as utils from './util'; 30 | import { QuickCreate } from './quickCreate'; 31 | import * as sdkConfig from './config'; 32 | import { invalidEmbedUrlErrorMessage } from './errors'; 33 | 34 | export interface IEvent { 35 | type: string; 36 | id: string; 37 | name: string; 38 | value: T; 39 | } 40 | 41 | /** 42 | * @hidden 43 | */ 44 | export interface ICustomEvent extends CustomEvent { 45 | detail: T; 46 | } 47 | 48 | /** 49 | * @hidden 50 | */ 51 | export interface IEventHandler { 52 | (event: ICustomEvent): any; 53 | } 54 | 55 | /** 56 | * @hidden 57 | */ 58 | export interface IHpmFactory { 59 | (wpmp: WindowPostMessageProxy, targetWindow?: Window, version?: string, type?: string, origin?: string): HttpPostMessage; 60 | } 61 | 62 | /** 63 | * @hidden 64 | */ 65 | export interface IWpmpFactory { 66 | (name?: string, logMessages?: boolean, eventSourceOverrideWindow?: Window): WindowPostMessageProxy; 67 | } 68 | 69 | /** 70 | * @hidden 71 | */ 72 | export interface IRouterFactory { 73 | (wpmp: WindowPostMessageProxy): Router; 74 | } 75 | 76 | export interface IPowerBiElement extends HTMLElement { 77 | powerBiEmbed: Embed; 78 | } 79 | 80 | export interface IDebugOptions { 81 | logMessages?: boolean; 82 | wpmpName?: string; 83 | } 84 | 85 | export interface IServiceConfiguration extends IDebugOptions { 86 | autoEmbedOnContentLoaded?: boolean; 87 | onError?: (error: any) => any; 88 | version?: string; 89 | type?: string; 90 | sdkWrapperVersion?: string; 91 | } 92 | 93 | export interface IService { 94 | hpm: HttpPostMessage; 95 | } 96 | 97 | export type IComponentEmbedConfiguration = IReportEmbedConfiguration | IDashboardEmbedConfiguration | ITileEmbedConfiguration | IVisualEmbedConfiguration | IQnaEmbedConfiguration; 98 | 99 | /** 100 | * @hidden 101 | */ 102 | export type EmbedComponentFactory = (service: Service, element: HTMLElement, config: IEmbedConfigurationBase, phasedRender?: boolean, isBootstrap?: boolean) => Embed; 103 | 104 | /** 105 | * The Power BI Service embed component, which is the entry point to embed all other Power BI components into your application 106 | * 107 | * @export 108 | * @class Service 109 | * @implements {IService} 110 | */ 111 | export class Service implements IService { 112 | 113 | /** 114 | * A list of components that this service can embed 115 | */ 116 | private static components: (typeof Report | typeof Tile | typeof Dashboard | typeof Qna | typeof Visual)[] = [ 117 | Tile, 118 | Report, 119 | Dashboard, 120 | Qna, 121 | Visual 122 | ]; 123 | 124 | /** 125 | * The default configuration for the service 126 | */ 127 | private static defaultConfig: IServiceConfiguration = { 128 | autoEmbedOnContentLoaded: false, 129 | onError: (...args) => console.log(args[0], args.slice(1)) 130 | }; 131 | 132 | /** 133 | * Gets or sets the access token as the global fallback token to use when a local token is not provided for a report or tile. 134 | * 135 | * @type {string} 136 | * @hidden 137 | */ 138 | accessToken: string; 139 | 140 | /** The Configuration object for the service*/ 141 | private config: IServiceConfiguration; 142 | 143 | /** A list of Power BI components that have been embedded using this service instance. */ 144 | private embeds: Embed[]; 145 | 146 | /** TODO: Look for way to make hpm private without sacrificing ease of maintenance. This should be private but in embed needs to call methods. 147 | * 148 | * @hidden 149 | */ 150 | hpm: HttpPostMessage; 151 | /** TODO: Look for way to make wpmp private. This is only public to allow stopping the wpmp in tests 152 | * 153 | * @hidden 154 | */ 155 | wpmp: WindowPostMessageProxy; 156 | router: Router; 157 | private uniqueSessionId: string; 158 | 159 | /** 160 | * @hidden 161 | */ 162 | private registeredComponents: { [componentType: string]: EmbedComponentFactory } = {}; 163 | 164 | /** 165 | * Creates an instance of a Power BI Service. 166 | * 167 | * @param {IHpmFactory} hpmFactory The http post message factory used in the postMessage communication layer 168 | * @param {IWpmpFactory} wpmpFactory The window post message factory used in the postMessage communication layer 169 | * @param {IRouterFactory} routerFactory The router factory used in the postMessage communication layer 170 | * @param {IServiceConfiguration} [config={}] 171 | * @hidden 172 | */ 173 | constructor(hpmFactory: IHpmFactory, wpmpFactory: IWpmpFactory, routerFactory: IRouterFactory, config: IServiceConfiguration = {}) { 174 | this.wpmp = wpmpFactory(config.wpmpName, config.logMessages); 175 | this.hpm = hpmFactory(this.wpmp, null, config.version, config.type, config.sdkWrapperVersion); 176 | this.router = routerFactory(this.wpmp); 177 | this.uniqueSessionId = utils.generateUUID(); 178 | 179 | /** 180 | * Adds handler for report events. 181 | */ 182 | this.router.post(`/reports/:uniqueId/events/:eventName`, (req, _res) => { 183 | const event: IEvent = { 184 | type: 'report', 185 | id: req.params.uniqueId as string, 186 | name: req.params.eventName as string, 187 | value: req.body 188 | }; 189 | 190 | this.handleEvent(event); 191 | }); 192 | 193 | this.router.post(`/reports/:uniqueId/pages/:pageName/events/:eventName`, (req, _res) => { 194 | const event: IEvent = { 195 | type: 'report', 196 | id: req.params.uniqueId as string, 197 | name: req.params.eventName as string, 198 | value: req.body 199 | }; 200 | 201 | this.handleEvent(event); 202 | }); 203 | 204 | this.router.post(`/reports/:uniqueId/pages/:pageName/visuals/:visualName/events/:eventName`, (req, _res) => { 205 | const event: IEvent = { 206 | type: 'report', 207 | id: req.params.uniqueId as string, 208 | name: req.params.eventName as string, 209 | value: req.body 210 | }; 211 | 212 | this.handleEvent(event); 213 | }); 214 | 215 | this.router.post(`/dashboards/:uniqueId/events/:eventName`, (req, _res) => { 216 | const event: IEvent = { 217 | type: 'dashboard', 218 | id: req.params.uniqueId as string, 219 | name: req.params.eventName as string, 220 | value: req.body 221 | }; 222 | 223 | this.handleEvent(event); 224 | }); 225 | 226 | this.router.post(`/tile/:uniqueId/events/:eventName`, (req, _res) => { 227 | const event: IEvent = { 228 | type: 'tile', 229 | id: req.params.uniqueId as string, 230 | name: req.params.eventName as string, 231 | value: req.body 232 | }; 233 | 234 | this.handleEvent(event); 235 | }); 236 | 237 | /** 238 | * Adds handler for Q&A events. 239 | */ 240 | this.router.post(`/qna/:uniqueId/events/:eventName`, (req, _res) => { 241 | const event: IEvent = { 242 | type: 'qna', 243 | id: req.params.uniqueId as string, 244 | name: req.params.eventName as string, 245 | value: req.body 246 | }; 247 | 248 | this.handleEvent(event); 249 | }); 250 | 251 | /** 252 | * Adds handler for front load 'ready' message. 253 | */ 254 | this.router.post(`/ready/:uniqueId`, (req, _res) => { 255 | const event: IEvent = { 256 | type: 'report', 257 | id: req.params.uniqueId as string, 258 | name: 'ready', 259 | value: req.body 260 | }; 261 | 262 | this.handleEvent(event); 263 | }); 264 | 265 | this.embeds = []; 266 | 267 | // TODO: Change when Object.assign is available. 268 | this.config = utils.assign({}, Service.defaultConfig, config); 269 | 270 | if (this.config.autoEmbedOnContentLoaded) { 271 | this.enableAutoEmbed(); 272 | } 273 | } 274 | 275 | /** 276 | * Creates new report 277 | * 278 | * @param {HTMLElement} element 279 | * @param {IEmbedConfiguration} [config={}] 280 | * @returns {Embed} 281 | */ 282 | createReport(element: HTMLElement, config: IEmbedConfiguration | IReportCreateConfiguration): Embed { 283 | config.type = 'create'; 284 | const powerBiElement = element as IPowerBiElement; 285 | const component = new Create(this, powerBiElement, config); 286 | powerBiElement.powerBiEmbed = component; 287 | this.addOrOverwriteEmbed(component, element); 288 | 289 | return component; 290 | } 291 | 292 | /** 293 | * Creates new dataset 294 | * 295 | * @param {HTMLElement} element 296 | * @param {IEmbedConfiguration} [config={}] 297 | * @returns {Embed} 298 | */ 299 | quickCreate(element: HTMLElement, config: IQuickCreateConfiguration): Embed { 300 | config.type = 'quickCreate'; 301 | const powerBiElement = element as IPowerBiElement; 302 | const component = new QuickCreate(this, powerBiElement, config); 303 | powerBiElement.powerBiEmbed = component; 304 | this.addOrOverwriteEmbed(component, element); 305 | 306 | return component; 307 | } 308 | 309 | /** 310 | * TODO: Add a description here 311 | * 312 | * @param {HTMLElement} [container] 313 | * @param {IEmbedConfiguration} [config=undefined] 314 | * @returns {Embed[]} 315 | * @hidden 316 | */ 317 | init(container?: HTMLElement, config: IEmbedConfiguration = undefined): Embed[] { 318 | container = (container && container instanceof HTMLElement) ? container : document.body; 319 | 320 | const elements = Array.prototype.slice.call(container.querySelectorAll(`[${Embed.embedUrlAttribute}]`)); 321 | return elements.map((element) => this.embed(element, config)); 322 | } 323 | 324 | /** 325 | * Given a configuration based on an HTML element, 326 | * if the component has already been created and attached to the element, reuses the component instance and existing iframe, 327 | * otherwise creates a new component instance. 328 | * 329 | * @param {HTMLElement} element 330 | * @param {IEmbedConfigurationBase} [config={}] 331 | * @returns {Embed} 332 | */ 333 | embed(element: HTMLElement, config: IComponentEmbedConfiguration | IEmbedConfigurationBase = {}): Embed { 334 | return this.embedInternal(element, config); 335 | } 336 | 337 | /** 338 | * Given a configuration based on an HTML element, 339 | * if the component has already been created and attached to the element, reuses the component instance and existing iframe, 340 | * otherwise creates a new component instance. 341 | * This is used for the phased embedding API, once element is loaded successfully, one can call 'render' on it. 342 | * 343 | * @param {HTMLElement} element 344 | * @param {IEmbedConfigurationBase} [config={}] 345 | * @returns {Embed} 346 | */ 347 | load(element: HTMLElement, config: IComponentEmbedConfiguration | IEmbedConfigurationBase = {}): Embed { 348 | return this.embedInternal(element, config, /* phasedRender */ true, /* isBootstrap */ false); 349 | } 350 | 351 | /** 352 | * Given an HTML element and entityType, creates a new component instance, and bootstrap the iframe for embedding. 353 | * 354 | * @param {HTMLElement} element 355 | * @param {IBootstrapEmbedConfiguration} config: a bootstrap config which is an embed config without access token. 356 | */ 357 | bootstrap(element: HTMLElement, config: IComponentEmbedConfiguration | IBootstrapEmbedConfiguration): Embed { 358 | return this.embedInternal(element, config, /* phasedRender */ false, /* isBootstrap */ true); 359 | } 360 | 361 | /** @hidden */ 362 | embedInternal(element: HTMLElement, config: IComponentEmbedConfiguration | IEmbedConfigurationBase = {}, phasedRender?: boolean, isBootstrap?: boolean): Embed { 363 | let component: Embed; 364 | const powerBiElement = element as IPowerBiElement; 365 | 366 | if (powerBiElement.powerBiEmbed) { 367 | if (isBootstrap) { 368 | throw new Error(`Attempted to bootstrap element ${element.outerHTML}, but the element is already a powerbi element.`); 369 | } 370 | 371 | component = this.embedExisting(powerBiElement, config, phasedRender); 372 | } 373 | else { 374 | component = this.embedNew(powerBiElement, config, phasedRender, isBootstrap); 375 | } 376 | 377 | return component; 378 | } 379 | 380 | /** @hidden */ 381 | getNumberOfComponents(): number { 382 | if (!this.embeds) { 383 | return 0; 384 | } 385 | 386 | return this.embeds.length; 387 | } 388 | 389 | /** @hidden */ 390 | getSdkSessionId(): string { 391 | return this.uniqueSessionId; 392 | } 393 | 394 | /** 395 | * Returns the Power BI Client SDK version 396 | * 397 | * @hidden 398 | */ 399 | getSDKVersion(): string { 400 | return sdkConfig.default.version; 401 | } 402 | 403 | /** 404 | * Given a configuration based on a Power BI element, saves the component instance that reference the element for later lookup. 405 | * 406 | * @private 407 | * @param {IPowerBiElement} element 408 | * @param {IEmbedConfigurationBase} config 409 | * @param {boolean} phasedRender 410 | * @param {boolean} isBootstrap 411 | * @returns {Embed} 412 | * @hidden 413 | */ 414 | private embedNew(element: IPowerBiElement, config: IComponentEmbedConfiguration | IEmbedConfigurationBase, phasedRender?: boolean, isBootstrap?: boolean): Embed { 415 | const componentType = config.type || element.getAttribute(Embed.typeAttribute); 416 | if (!componentType) { 417 | const scrubbedConfig = { ...config, accessToken: "" }; 418 | throw new Error(`Attempted to embed using config ${JSON.stringify(scrubbedConfig)} on element ${element.outerHTML}, but could not determine what type of component to embed. You must specify a type in the configuration or as an attribute such as '${Embed.typeAttribute}="${Report.type.toLowerCase()}"'.`); 419 | } 420 | 421 | // Saves the type as part of the configuration so that it can be referenced later at a known location. 422 | config.type = componentType; 423 | 424 | const component = this.createEmbedComponent(componentType, element, config, phasedRender, isBootstrap); 425 | element.powerBiEmbed = component; 426 | 427 | this.addOrOverwriteEmbed(component, element); 428 | return component; 429 | } 430 | 431 | /** 432 | * Given component type, creates embed component instance 433 | * 434 | * @private 435 | * @param {string} componentType 436 | * @param {HTMLElement} element 437 | * @param {IEmbedConfigurationBase} config 438 | * @param {boolean} phasedRender 439 | * @param {boolean} isBootstrap 440 | * @returns {Embed} 441 | * @hidden 442 | */ 443 | private createEmbedComponent(componentType: string, element: HTMLElement, config: IEmbedConfigurationBase, phasedRender?: boolean, isBootstrap?: boolean): Embed { 444 | const Component = utils.find((embedComponent) => componentType === embedComponent.type.toLowerCase(), Service.components); 445 | if (Component) { 446 | return new Component(this, element, config, phasedRender, isBootstrap); 447 | } 448 | 449 | // If component type is not legacy, search in registered components 450 | const registeredComponent = utils.find((registeredComponentType) => componentType.toLowerCase() === registeredComponentType.toLowerCase(), Object.keys(this.registeredComponents)); 451 | if (!registeredComponent) { 452 | throw new Error(`Attempted to embed component of type: ${componentType} but did not find any matching component. Please verify the type you specified is intended.`); 453 | } 454 | 455 | return this.registeredComponents[registeredComponent](this, element, config, phasedRender, isBootstrap); 456 | } 457 | 458 | /** 459 | * Given an element that already contains an embed component, load with a new configuration. 460 | * 461 | * @private 462 | * @param {IPowerBiElement} element 463 | * @param {IEmbedConfigurationBase} config 464 | * @returns {Embed} 465 | * @hidden 466 | */ 467 | private embedExisting(element: IPowerBiElement, config: IComponentEmbedConfiguration | IEmbedConfigurationBase, phasedRender?: boolean): Embed { 468 | const component = utils.find((x) => x.element === element, this.embeds); 469 | if (!component) { 470 | const scrubbedConfig = { ...config, accessToken: "" }; 471 | throw new Error(`Attempted to embed using config ${JSON.stringify(scrubbedConfig)} on element ${element.outerHTML} which already has embedded component associated, but could not find the existing component in the list of active components. This could indicate the embeds list is out of sync with the DOM, or the component is referencing the incorrect HTML element.`); 472 | } 473 | 474 | // TODO: Multiple embedding to the same iframe is not supported in QnA 475 | if (config.type && config.type.toLowerCase() === "qna") { 476 | return this.embedNew(element, config); 477 | } 478 | 479 | /** 480 | * TODO: Dynamic embed type switching could be supported but there is work needed to prepare the service state and DOM cleanup. 481 | * remove all event handlers from the DOM, then reset the element to initial state which removes iframe, and removes from list of embeds 482 | * then we can call the embedNew function which would allow setting the proper embedUrl and construction of object based on the new type. 483 | */ 484 | if (typeof config.type === "string" && config.type !== component.config.type) { 485 | 486 | /** 487 | * When loading report after create we want to use existing Iframe to optimize load period 488 | */ 489 | if (config.type === "report" && utils.isCreate(component.config.type)) { 490 | const report = new Report(this, element, config, /* phasedRender */ false, /* isBootstrap */ false, element.powerBiEmbed.iframe); 491 | component.populateConfig(config, /* isBootstrap */ false); 492 | report.load(); 493 | element.powerBiEmbed = report; 494 | 495 | this.addOrOverwriteEmbed(component, element); 496 | 497 | return report; 498 | } 499 | const scrubbedConfig = { ...config, accessToken: "" }; 500 | throw new Error(`Embedding on an existing element with a different type than the previous embed object is not supported. Attempted to embed using config ${JSON.stringify(scrubbedConfig)} on element ${element.outerHTML}, but the existing element contains an embed of type: ${this.config.type} which does not match the new type: ${config.type}`); 501 | } 502 | 503 | component.populateConfig(config, /* isBootstrap */ false); 504 | component.load(phasedRender); 505 | 506 | return component; 507 | } 508 | 509 | /** 510 | * Adds an event handler for DOMContentLoaded, which searches the DOM for elements that have the 'powerbi-embed-url' attribute, 511 | * and automatically attempts to embed a powerbi component based on information from other powerbi-* attributes. 512 | * 513 | * Note: Only runs if `config.autoEmbedOnContentLoaded` is true when the service is created. 514 | * This handler is typically useful only for applications that are rendered on the server so that all required data is available when the handler is called. 515 | * 516 | * @hidden 517 | */ 518 | enableAutoEmbed(): void { 519 | window.addEventListener('DOMContentLoaded', (_event: Event) => this.init(document.body), false); 520 | } 521 | 522 | /** 523 | * Returns an instance of the component associated with the element. 524 | * 525 | * @param {HTMLElement} element 526 | * @returns {(Report | Tile)} 527 | */ 528 | get(element: HTMLElement): Embed { 529 | const powerBiElement = element as IPowerBiElement; 530 | 531 | if (!powerBiElement.powerBiEmbed) { 532 | throw new Error(`You attempted to get an instance of powerbi component associated with element: ${element.outerHTML} but there was no associated instance.`); 533 | } 534 | 535 | return powerBiElement.powerBiEmbed; 536 | } 537 | 538 | /** 539 | * Finds an embed instance by the name or unique ID that is provided. 540 | * 541 | * @param {string} uniqueId 542 | * @returns {(Report | Tile)} 543 | * @hidden 544 | */ 545 | find(uniqueId: string): Embed { 546 | return utils.find((x) => x.config.uniqueId === uniqueId, this.embeds); 547 | } 548 | 549 | /** 550 | * Removes embed components whose container element is same as the given element 551 | * 552 | * @param {Embed} component 553 | * @param {HTMLElement} element 554 | * @returns {void} 555 | * @hidden 556 | */ 557 | addOrOverwriteEmbed(component: Embed, element: HTMLElement): void { 558 | // remove embeds over the same div element. 559 | this.embeds = this.embeds.filter(function (embed) { 560 | return embed.element !== element; 561 | }); 562 | 563 | this.embeds.push(component); 564 | } 565 | 566 | /** 567 | * Given an HTML element that has a component embedded within it, removes the component from the list of embedded components, removes the association between the element and the component, and removes the iframe. 568 | * 569 | * @param {HTMLElement} element 570 | * @returns {void} 571 | */ 572 | reset(element: HTMLElement): void { 573 | const powerBiElement = element as IPowerBiElement; 574 | 575 | if (!powerBiElement.powerBiEmbed) { 576 | return; 577 | } 578 | 579 | /** Removes the element frontLoad listener if exists. */ 580 | const embedElement = powerBiElement.powerBiEmbed; 581 | if (embedElement.frontLoadHandler) { 582 | embedElement.element.removeEventListener('ready', embedElement.frontLoadHandler, false); 583 | } 584 | 585 | /** Removes all event handlers. */ 586 | embedElement.allowedEvents.forEach((eventName) => { 587 | embedElement.off(eventName); 588 | }); 589 | 590 | /** Removes the component from an internal list of components. */ 591 | utils.remove((x) => x === powerBiElement.powerBiEmbed, this.embeds); 592 | /** Deletes a property from the HTML element. */ 593 | delete powerBiElement.powerBiEmbed; 594 | /** Removes the iframe from the element. */ 595 | const iframe = element.querySelector('iframe'); 596 | if (iframe) { 597 | if (iframe.remove !== undefined) { 598 | iframe.remove(); 599 | } 600 | else { 601 | /** Workaround for IE: unhandled rejection TypeError: object doesn't support property or method 'remove' */ 602 | iframe.parentElement.removeChild(iframe); 603 | } 604 | } 605 | } 606 | 607 | /** 608 | * handles tile events 609 | * 610 | * @param {IEvent} event 611 | * @hidden 612 | */ 613 | handleTileEvents(event: IEvent): void { 614 | if (event.type === 'tile') { 615 | this.handleEvent(event); 616 | } 617 | } 618 | 619 | async invokeSDKHook(hook: Function, req: IExtendedRequest, res: IExtendedResponse): Promise { 620 | if (!hook) { 621 | res.send(404, null); 622 | return; 623 | } 624 | 625 | try { 626 | let result = await hook(req.body); 627 | res.send(200, result); 628 | } catch (error) { 629 | res.send(400, null); 630 | console.error(error); 631 | } 632 | } 633 | 634 | /** 635 | * Given an event object, finds the embed component with the matching type and ID, and invokes its handleEvent method with the event object. 636 | * 637 | * @private 638 | * @param {IEvent} event 639 | * @hidden 640 | */ 641 | private handleEvent(event: IEvent): void { 642 | const embed = utils.find(embed => { 643 | return (embed.config.uniqueId === event.id); 644 | }, this.embeds); 645 | 646 | if (embed) { 647 | const value = event.value; 648 | 649 | if (event.name === 'pageChanged') { 650 | const pageKey = 'newPage'; 651 | const page: IPage = value[pageKey]; 652 | if (!page) { 653 | throw new Error(`Page model not found at 'event.value.${pageKey}'.`); 654 | } 655 | value[pageKey] = new Page(embed, page.name, page.displayName, true /* isActive */); 656 | } 657 | 658 | utils.raiseCustomEvent(embed.element, event.name, value); 659 | } 660 | } 661 | 662 | /** 663 | * API for warm starting powerbi embedded endpoints. 664 | * Use this API to preload Power BI Embedded in the background. 665 | * 666 | * @public 667 | * @param {IEmbedConfigurationBase} [config={}] 668 | * @param {HTMLElement} [element=undefined] 669 | */ 670 | preload(config: IComponentEmbedConfiguration | IEmbedConfigurationBase, element?: HTMLElement): HTMLIFrameElement { 671 | if (!utils.validateEmbedUrl(config.embedUrl)) { 672 | throw new Error(invalidEmbedUrlErrorMessage); 673 | } 674 | 675 | const iframeContent = document.createElement("iframe"); 676 | iframeContent.setAttribute("style", "display:none;"); 677 | iframeContent.setAttribute("src", config.embedUrl); 678 | iframeContent.setAttribute("scrolling", "no"); 679 | iframeContent.setAttribute("allowfullscreen", "false"); 680 | 681 | let node = element; 682 | if (!node) { 683 | node = document.getElementsByTagName("body")[0]; 684 | } 685 | 686 | node.appendChild(iframeContent); 687 | iframeContent.onload = () => { 688 | utils.raiseCustomEvent(iframeContent, "preloaded", {}); 689 | }; 690 | 691 | return iframeContent; 692 | } 693 | 694 | /** 695 | * Use this API to set SDK info 696 | * 697 | * @hidden 698 | * @param {string} type 699 | * @returns {void} 700 | */ 701 | setSdkInfo(type: string, version: string): void { 702 | this.hpm.defaultHeaders['x-sdk-type'] = type; 703 | this.hpm.defaultHeaders['x-sdk-wrapper-version'] = version; 704 | } 705 | 706 | /** 707 | * API for registering external components 708 | * 709 | * @hidden 710 | * @param {string} componentType 711 | * @param {EmbedComponentFactory} embedComponentFactory 712 | * @param {string[]} routerEventUrls 713 | */ 714 | register(componentType: string, embedComponentFactory: EmbedComponentFactory, routerEventUrls: string[]): void { 715 | if (utils.find((embedComponent) => componentType.toLowerCase() === embedComponent.type.toLowerCase(), Service.components)) { 716 | throw new Error('The component name is reserved. Cannot register a component with this name.'); 717 | } 718 | 719 | if (utils.find((registeredComponentType) => componentType.toLowerCase() === registeredComponentType.toLowerCase(), Object.keys(this.registeredComponents))) { 720 | throw new Error('A component with this type is already registered.'); 721 | } 722 | 723 | this.registeredComponents[componentType] = embedComponentFactory; 724 | 725 | routerEventUrls.forEach(url => { 726 | if (!url.includes(':uniqueId') || !url.includes(':eventName')) { 727 | throw new Error('Invalid router event URL'); 728 | } 729 | 730 | this.router.post(url, (req, _res) => { 731 | const event: IEvent = { 732 | type: componentType, 733 | id: req.params.uniqueId as string, 734 | name: req.params.eventName as string, 735 | value: req.body 736 | }; 737 | 738 | this.handleEvent(event); 739 | }); 740 | }); 741 | } 742 | } 743 | -------------------------------------------------------------------------------- /src/tile.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | import { IError, validateTileLoad } from 'powerbi-models'; 5 | import { Service } from './service'; 6 | import { Embed, IEmbedConfigurationBase, ITileEmbedConfiguration } from './embed'; 7 | 8 | /** 9 | * The Power BI tile embed component 10 | * 11 | * @export 12 | * @class Tile 13 | * @extends {Embed} 14 | */ 15 | export class Tile extends Embed { 16 | /** @hidden */ 17 | static type = "Tile"; 18 | /** @hidden */ 19 | static allowedEvents = ["tileClicked", "tileLoaded"]; 20 | 21 | /** 22 | * @hidden 23 | */ 24 | constructor(service: Service, element: HTMLElement, baseConfig: IEmbedConfigurationBase, phasedRender?: boolean, isBootstrap?: boolean) { 25 | const config = baseConfig as ITileEmbedConfiguration; 26 | super(service, element, config, /* iframe */ undefined, phasedRender, isBootstrap); 27 | this.loadPath = "/tile/load"; 28 | Array.prototype.push.apply(this.allowedEvents, Tile.allowedEvents); 29 | } 30 | 31 | /** 32 | * The ID of the tile 33 | * 34 | * @returns {string} 35 | */ 36 | getId(): string { 37 | const config = this.config as ITileEmbedConfiguration; 38 | const tileId = config.id || Tile.findIdFromEmbedUrl(this.config.embedUrl); 39 | 40 | if (typeof tileId !== 'string' || tileId.length === 0) { 41 | throw new Error(`Tile id is required, but it was not found. You must provide an id either as part of embed configuration.`); 42 | } 43 | 44 | return tileId; 45 | } 46 | 47 | /** 48 | * Validate load configuration. 49 | */ 50 | validate(config: IEmbedConfigurationBase): IError[] { 51 | const embedConfig = config as ITileEmbedConfiguration; 52 | return validateTileLoad(embedConfig); 53 | } 54 | 55 | /** 56 | * Handle config changes. 57 | * 58 | * @returns {void} 59 | */ 60 | configChanged(isBootstrap: boolean): void { 61 | if (isBootstrap) { 62 | return; 63 | } 64 | 65 | // Populate tile id into config object. 66 | (this.config as ITileEmbedConfiguration).id = this.getId(); 67 | } 68 | 69 | /** 70 | * @hidden 71 | * @returns {string} 72 | */ 73 | getDefaultEmbedUrlEndpoint(): string { 74 | return "tileEmbed"; 75 | } 76 | 77 | /** 78 | * Adds the ability to get tileId from url. 79 | * By extracting the ID we can ensure that the ID is always explicitly provided as part of the load configuration. 80 | * 81 | * @hidden 82 | * @static 83 | * @param {string} url 84 | * @returns {string} 85 | */ 86 | static findIdFromEmbedUrl(url: string): string { 87 | const tileIdRegEx = /tileId="?([^&]+)"?/; 88 | const tileIdMatch = url.match(tileIdRegEx); 89 | 90 | let tileId: string; 91 | if (tileIdMatch) { 92 | tileId = tileIdMatch[1]; 93 | } 94 | 95 | return tileId; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | import { HttpPostMessage } from 'http-post-message'; 5 | 6 | /** 7 | * @hidden 8 | */ 9 | const allowedPowerBiHostsRegex = 10 | new RegExp(/(.+\.powerbi\.com$)|(.+\.fabric\.microsoft\.com$)|(.+\.analysis\.windows-int\.net$)|(.+\.analysis-df\.windows\.net$)/); 11 | 12 | /** 13 | * @hidden 14 | */ 15 | const allowedPowerBiHostsSovRegex = new RegExp(/^app\.powerbi\.cn$|^app(\.mil\.|\.high\.|\.)powerbigov\.us$|^app\.powerbi\.eaglex\.ic\.gov$|^app\.powerbi\.microsoft\.scloud$/); 16 | 17 | /** 18 | * @hidden 19 | */ 20 | const expectedEmbedUrlProtocol: string = "https:"; 21 | 22 | /** 23 | * Raises a custom event with event data on the specified HTML element. 24 | * 25 | * @export 26 | * @param {HTMLElement} element 27 | * @param {string} eventName 28 | * @param {*} eventData 29 | */ 30 | export function raiseCustomEvent(element: HTMLElement, eventName: string, eventData: any): void { 31 | let customEvent: CustomEvent; 32 | if (typeof CustomEvent === 'function') { 33 | customEvent = new CustomEvent(eventName, { 34 | detail: eventData, 35 | bubbles: true, 36 | cancelable: true 37 | }); 38 | } else { 39 | customEvent = document.createEvent('CustomEvent'); 40 | customEvent.initCustomEvent(eventName, true, true, eventData); 41 | } 42 | 43 | element.dispatchEvent(customEvent); 44 | } 45 | 46 | /** 47 | * Finds the index of the first value in an array that matches the specified predicate. 48 | * 49 | * @export 50 | * @template T 51 | * @param {(x: T) => boolean} predicate 52 | * @param {T[]} xs 53 | * @returns {number} 54 | */ 55 | export function findIndex(predicate: (x: T) => boolean, xs: T[]): number { 56 | if (!Array.isArray(xs)) { 57 | throw new Error(`You attempted to call find with second parameter that was not an array. You passed: ${xs}`); 58 | } 59 | 60 | let index: number; 61 | xs.some((x, i) => { 62 | if (predicate(x)) { 63 | index = i; 64 | return true; 65 | } 66 | }); 67 | 68 | return index; 69 | } 70 | 71 | /** 72 | * Finds the first value in an array that matches the specified predicate. 73 | * 74 | * @export 75 | * @template T 76 | * @param {(x: T) => boolean} predicate 77 | * @param {T[]} xs 78 | * @returns {T} 79 | */ 80 | export function find(predicate: (x: T) => boolean, xs: T[]): T { 81 | const index = findIndex(predicate, xs); 82 | return xs[index]; 83 | } 84 | 85 | export function remove(predicate: (x: T) => boolean, xs: T[]): void { 86 | const index = findIndex(predicate, xs); 87 | xs.splice(index, 1); 88 | } 89 | 90 | // See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign 91 | // TODO: replace in favor of using polyfill 92 | /** 93 | * Copies the values of all enumerable properties from one or more source objects to a target object, and returns the target object. 94 | * 95 | * @export 96 | * @param {any} args 97 | * @returns 98 | */ 99 | export function assign(...args): any { 100 | var target = args[0]; 101 | 102 | 'use strict'; 103 | if (target === undefined || target === null) { 104 | throw new TypeError('Cannot convert undefined or null to object'); 105 | } 106 | 107 | var output = Object(target); 108 | for (var index = 1; index < arguments.length; index++) { 109 | var source = arguments[index]; 110 | if (source !== undefined && source !== null) { 111 | for (var nextKey in source) { 112 | if (source.hasOwnProperty(nextKey)) { 113 | output[nextKey] = source[nextKey]; 114 | } 115 | } 116 | } 117 | } 118 | return output; 119 | } 120 | 121 | /** 122 | * Generates a random 5 to 6 character string. 123 | * 124 | * @export 125 | * @returns {string} 126 | */ 127 | export function createRandomString(): string { 128 | return getRandomValue().toString(36).substring(1); 129 | } 130 | 131 | /** 132 | * Generates a 20 character uuid. 133 | * 134 | * @export 135 | * @returns {string} 136 | */ 137 | export function generateUUID(): string { 138 | let d = new Date().getTime(); 139 | if (typeof performance !== 'undefined' && typeof performance.now === 'function') { 140 | d += performance.now(); 141 | } 142 | return 'xxxxxxxxxxxxxxxxxxxx'.replace(/[xy]/g, function (_c) { 143 | // Generate a random number, scaled from 0 to 15. 144 | const r = (getRandomValue() % 16); 145 | 146 | // Shift 4 times to divide by 16 147 | d >>= 4; 148 | return r.toString(16); 149 | }); 150 | } 151 | 152 | /** 153 | * Adds a parameter to the given url 154 | * 155 | * @export 156 | * @param {string} url 157 | * @param {string} paramName 158 | * @param {string} value 159 | * @returns {string} 160 | */ 161 | export function addParamToUrl(url: string, paramName: string, value: string): string { 162 | const parameterPrefix = url.indexOf('?') > 0 ? '&' : '?'; 163 | url += parameterPrefix + paramName + '=' + value; 164 | return url; 165 | } 166 | 167 | /** 168 | * Checks if the report is saved. 169 | * 170 | * @export 171 | * @param {HttpPostMessage} hpm 172 | * @param {string} uid 173 | * @param {Window} contentWindow 174 | * @returns {Promise} 175 | */ 176 | export async function isSavedInternal(hpm: HttpPostMessage, uid: string, contentWindow: Window): Promise { 177 | try { 178 | const response = await hpm.get('/report/hasUnsavedChanges', { uid: uid }, contentWindow); 179 | return !response.body; 180 | } catch (response) { 181 | throw response.body; 182 | } 183 | } 184 | 185 | /** 186 | * Checks if the embed url is for RDL report. 187 | * 188 | * @export 189 | * @param {string} embedUrl 190 | * @returns {boolean} 191 | */ 192 | export function isRDLEmbed(embedUrl: string): boolean { 193 | return embedUrl && embedUrl.toLowerCase().indexOf("/rdlembed?") >= 0; 194 | } 195 | 196 | /** 197 | * Checks if the embed url contains autoAuth=true. 198 | * 199 | * @export 200 | * @param {string} embedUrl 201 | * @returns {boolean} 202 | */ 203 | export function autoAuthInEmbedUrl(embedUrl: string): boolean { 204 | return embedUrl && decodeURIComponent(embedUrl).toLowerCase().indexOf("autoauth=true") >= 0; 205 | } 206 | 207 | /** 208 | * Returns random number 209 | */ 210 | export function getRandomValue(): number { 211 | 212 | // window.msCrypto for IE 213 | const cryptoObj = window.crypto || (window as any).msCrypto; 214 | const randomValueArray = new Uint32Array(1); 215 | cryptoObj.getRandomValues(randomValueArray); 216 | 217 | return randomValueArray[0]; 218 | } 219 | 220 | /** 221 | * Returns the time interval between two dates in milliseconds 222 | * 223 | * @export 224 | * @param {Date} start 225 | * @param {Date} end 226 | * @returns {number} 227 | */ 228 | export function getTimeDiffInMilliseconds(start: Date, end: Date): number { 229 | return Math.abs(start.getTime() - end.getTime()); 230 | } 231 | 232 | /** 233 | * Checks if the embed type is for create 234 | * 235 | * @export 236 | * @param {string} embedType 237 | * @returns {boolean} 238 | */ 239 | export function isCreate(embedType: string): boolean { 240 | return embedType === 'create' || embedType === 'quickcreate'; 241 | } 242 | 243 | /** 244 | * Checks if the embedUrl has an allowed power BI domain 245 | * @hidden 246 | */ 247 | export function validateEmbedUrl(embedUrl: string): boolean { 248 | if (embedUrl) { 249 | let url: URL; 250 | try { 251 | url = new URL(embedUrl.toLowerCase()); 252 | } catch(e) { 253 | // invalid URL 254 | return false; 255 | } 256 | return url.protocol === expectedEmbedUrlProtocol && 257 | (allowedPowerBiHostsRegex.test(url.hostname) || allowedPowerBiHostsSovRegex.test(url.hostname)); 258 | } 259 | } -------------------------------------------------------------------------------- /src/visual.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | import { 5 | DisplayOption, 6 | FiltersLevel, 7 | FiltersOperations, 8 | ICustomPageSize, 9 | IEmbedConfigurationBase, 10 | IError, 11 | IFilter, 12 | IReportEmbedConfiguration, 13 | IReportLoadConfiguration, 14 | IUpdateFiltersRequest, 15 | IVisual, 16 | IVisualEmbedConfiguration, 17 | LayoutType, 18 | PageLevelFilters, 19 | PageSizeType, 20 | PagesLayout, 21 | ReportLevelFilters, 22 | VisualContainerDisplayMode, 23 | VisualLevelFilters 24 | } from 'powerbi-models'; 25 | import { IHttpPostMessageResponse } from 'http-post-message'; 26 | import { Service } from './service'; 27 | import { Report } from './report'; 28 | import { Page } from './page'; 29 | import { VisualDescriptor } from './visualDescriptor'; 30 | 31 | /** 32 | * The Power BI Visual embed component 33 | * 34 | * @export 35 | * @class Visual 36 | */ 37 | export class Visual extends Report { 38 | /** @hidden */ 39 | static type = "visual"; 40 | 41 | /** @hidden */ 42 | static GetPagesNotSupportedError = "Get pages is not supported while embedding a visual."; 43 | /** @hidden */ 44 | static SetPageNotSupportedError = "Set page is not supported while embedding a visual."; 45 | /** @hidden */ 46 | static RenderNotSupportedError = "render is not supported while embedding a visual."; 47 | 48 | /** 49 | * Creates an instance of a Power BI Single Visual. 50 | * 51 | * @param {Service} service 52 | * @param {HTMLElement} element 53 | * @param {IEmbedConfiguration} config 54 | * @hidden 55 | */ 56 | constructor(service: Service, element: HTMLElement, baseConfig: IEmbedConfigurationBase, phasedRender?: boolean, isBootstrap?: boolean, iframe?: HTMLIFrameElement) { 57 | super(service, element, baseConfig, phasedRender, isBootstrap, iframe); 58 | } 59 | 60 | /** 61 | * @hidden 62 | */ 63 | load(phasedRender?: boolean): Promise { 64 | const config = this.config as IVisualEmbedConfiguration; 65 | 66 | if (!config.accessToken) { 67 | // bootstrap flow. 68 | return; 69 | } 70 | 71 | if (typeof config.pageName !== 'string' || config.pageName.length === 0) { 72 | throw new Error(`Page name is required when embedding a visual.`); 73 | } 74 | 75 | if (typeof config.visualName !== 'string' || config.visualName.length === 0) { 76 | throw new Error(`Visual name is required, but it was not found. You must provide a visual name as part of embed configuration.`); 77 | } 78 | 79 | // calculate custom layout settings and override config. 80 | const width = config.width ? config.width : this.iframe.offsetWidth; 81 | const height = config.height ? config.height : this.iframe.offsetHeight; 82 | 83 | const pageSize: ICustomPageSize = { 84 | type: PageSizeType.Custom, 85 | width: width, 86 | height: height, 87 | }; 88 | 89 | const pagesLayout: PagesLayout = {}; 90 | pagesLayout[config.pageName] = { 91 | defaultLayout: { 92 | displayState: { 93 | mode: VisualContainerDisplayMode.Hidden 94 | } 95 | }, 96 | visualsLayout: {} 97 | }; 98 | 99 | pagesLayout[config.pageName].visualsLayout[config.visualName] = { 100 | displayState: { 101 | mode: VisualContainerDisplayMode.Visible 102 | }, 103 | x: 1, 104 | y: 1, 105 | z: 1, 106 | width: pageSize.width, 107 | height: pageSize.height 108 | }; 109 | 110 | config.settings = config.settings || {}; 111 | config.settings.filterPaneEnabled = false; 112 | config.settings.navContentPaneEnabled = false; 113 | config.settings.layoutType = LayoutType.Custom; 114 | config.settings.customLayout = { 115 | displayOption: config.settings?.customLayout?.displayOption ?? DisplayOption.FitToPage, 116 | pageSize: config.settings?.customLayout?.pageSize ?? pageSize, 117 | pagesLayout: config.settings?.customLayout?.pagesLayout ?? pagesLayout 118 | }; 119 | 120 | this.config = config; 121 | return super.load(phasedRender); 122 | } 123 | 124 | /** 125 | * Gets the list of pages within the report - not supported in visual 126 | * 127 | * @returns {Promise} 128 | */ 129 | getPages(): Promise { 130 | throw Visual.GetPagesNotSupportedError; 131 | } 132 | 133 | /** 134 | * Sets the active page of the report - not supported in visual 135 | * 136 | * @param {string} pageName 137 | * @returns {Promise>} 138 | */ 139 | setPage(_pageName: string): Promise> { 140 | throw Visual.SetPageNotSupportedError; 141 | } 142 | 143 | /** 144 | * Render a preloaded report, using phased embedding API 145 | * 146 | * @hidden 147 | * @returns {Promise} 148 | */ 149 | async render(_config?: IReportLoadConfiguration | IReportEmbedConfiguration): Promise { 150 | throw Visual.RenderNotSupportedError; 151 | } 152 | 153 | /** 154 | * Gets the embedded visual descriptor object that contains the visual name, type, etc. 155 | * 156 | * ```javascript 157 | * visual.getVisualDescriptor() 158 | * .then(visualDetails => { ... }); 159 | * ``` 160 | * 161 | * @returns {Promise} 162 | */ 163 | async getVisualDescriptor(): Promise { 164 | const config = this.config as IVisualEmbedConfiguration; 165 | 166 | try { 167 | const response = await this.service.hpm.get(`/report/pages/${config.pageName}/visuals`, { uid: this.config.uniqueId }, this.iframe.contentWindow); 168 | // Find the embedded visual from visuals of this page 169 | // TODO: Use the Array.find method when ES6 is available 170 | const embeddedVisuals = response.body.filter((pageVisual) => pageVisual.name === config.visualName); 171 | 172 | if (embeddedVisuals.length === 0) { 173 | const visualNotFoundError: IError = { 174 | message: "visualNotFound", 175 | detailedMessage: "Visual not found" 176 | }; 177 | 178 | throw visualNotFoundError; 179 | } 180 | 181 | const embeddedVisual = embeddedVisuals[0]; 182 | const currentPage = this.page(config.pageName); 183 | return new VisualDescriptor(currentPage, embeddedVisual.name, embeddedVisual.title, embeddedVisual.type, embeddedVisual.layout); 184 | } catch (response) { 185 | throw response.body; 186 | } 187 | } 188 | 189 | /** 190 | * Gets filters that are applied to the filter level. 191 | * Default filter level is visual level. 192 | * 193 | * ```javascript 194 | * visual.getFilters(filtersLevel) 195 | * .then(filters => { 196 | * ... 197 | * }); 198 | * ``` 199 | * 200 | * @returns {Promise} 201 | */ 202 | async getFilters(filtersLevel?: FiltersLevel): Promise { 203 | const url: string = this.getFiltersLevelUrl(filtersLevel); 204 | try { 205 | const response = await this.service.hpm.get(url, { uid: this.config.uniqueId }, this.iframe.contentWindow); 206 | return response.body; 207 | } catch (response) { 208 | throw response.body; 209 | } 210 | } 211 | 212 | /** 213 | * Updates filters at the filter level. 214 | * Default filter level is visual level. 215 | * 216 | * ```javascript 217 | * const filters: [ 218 | * ... 219 | * ]; 220 | * 221 | * visual.updateFilters(FiltersOperations.Add, filters, filtersLevel) 222 | * .catch(errors => { 223 | * ... 224 | * }); 225 | * ``` 226 | * 227 | * @param {(IFilter[])} filters 228 | * @returns {Promise>} 229 | */ 230 | async updateFilters(operation: FiltersOperations, filters: IFilter[], filtersLevel?: FiltersLevel): Promise> { 231 | const updateFiltersRequest: IUpdateFiltersRequest = { 232 | filtersOperation: operation, 233 | filters: filters as VisualLevelFilters[] | PageLevelFilters[] | ReportLevelFilters[] 234 | }; 235 | 236 | const url: string = this.getFiltersLevelUrl(filtersLevel); 237 | try { 238 | return await this.service.hpm.post(url, updateFiltersRequest, { uid: this.config.uniqueId }, this.iframe.contentWindow); 239 | } catch (response) { 240 | throw response.body; 241 | } 242 | } 243 | 244 | /** 245 | * Sets filters at the filter level. 246 | * Default filter level is visual level. 247 | * 248 | * ```javascript 249 | * const filters: [ 250 | * ... 251 | * ]; 252 | * 253 | * visual.setFilters(filters, filtersLevel) 254 | * .catch(errors => { 255 | * ... 256 | * }); 257 | * ``` 258 | * 259 | * @param {(IFilter[])} filters 260 | * @returns {Promise>} 261 | */ 262 | async setFilters(filters: IFilter[], filtersLevel?: FiltersLevel): Promise> { 263 | const url: string = this.getFiltersLevelUrl(filtersLevel); 264 | try { 265 | return await this.service.hpm.put(url, filters, { uid: this.config.uniqueId }, this.iframe.contentWindow); 266 | } catch (response) { 267 | throw response.body; 268 | } 269 | } 270 | 271 | /** 272 | * Removes all filters from the current filter level. 273 | * Default filter level is visual level. 274 | * 275 | * ```javascript 276 | * visual.removeFilters(filtersLevel); 277 | * ``` 278 | * 279 | * @returns {Promise>} 280 | */ 281 | async removeFilters(filtersLevel?: FiltersLevel): Promise> { 282 | return await this.updateFilters(FiltersOperations.RemoveAll, undefined, filtersLevel); 283 | } 284 | 285 | /** 286 | * @hidden 287 | */ 288 | private getFiltersLevelUrl(filtersLevel: FiltersLevel): string { 289 | const config = this.config as IVisualEmbedConfiguration; 290 | switch (filtersLevel) { 291 | case FiltersLevel.Report: 292 | return `/report/filters`; 293 | case FiltersLevel.Page: 294 | return `/report/pages/${config.pageName}/filters`; 295 | default: 296 | return `/report/pages/${config.pageName}/visuals/${config.visualName}/filters`; 297 | } 298 | } 299 | } 300 | -------------------------------------------------------------------------------- /src/visualDescriptor.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | import { 5 | ExportDataType, 6 | FiltersOperations, 7 | ICloneVisualRequest, 8 | ICloneVisualResponse, 9 | IExportDataRequest, 10 | IExportDataResult, 11 | IFilter, 12 | ISlicerState, 13 | ISmartNarratives, 14 | ISortByVisualRequest, 15 | IUpdateFiltersRequest, 16 | IVisualLayout, 17 | VisualContainerDisplayMode, 18 | VisualLevelFilters, 19 | } from 'powerbi-models'; 20 | import { IHttpPostMessageResponse } from 'http-post-message'; 21 | import { IFilterable } from './ifilterable'; 22 | import { IPageNode } from './page'; 23 | import { Report } from './report'; 24 | 25 | /** 26 | * A Visual node within a report hierarchy 27 | * 28 | * @export 29 | * @interface IVisualNode 30 | */ 31 | export interface IVisualNode { 32 | name: string; 33 | title: string; 34 | type: string; 35 | layout: IVisualLayout; 36 | page: IPageNode; 37 | } 38 | 39 | /** 40 | * A Power BI visual within a page 41 | * 42 | * @export 43 | * @class VisualDescriptor 44 | * @implements {IVisualNode} 45 | */ 46 | export class VisualDescriptor implements IVisualNode, IFilterable { 47 | /** 48 | * The visual name 49 | * 50 | * @type {string} 51 | */ 52 | name: string; 53 | 54 | /** 55 | * The visual title 56 | * 57 | * @type {string} 58 | */ 59 | title: string; 60 | 61 | /** 62 | * The visual type 63 | * 64 | * @type {string} 65 | */ 66 | type: string; 67 | 68 | /** 69 | * The visual layout: position, size and visibility. 70 | * 71 | * @type {string} 72 | */ 73 | layout: IVisualLayout; 74 | 75 | /** 76 | * The parent Power BI page that contains this visual 77 | * 78 | * @type {IPageNode} 79 | */ 80 | page: IPageNode; 81 | 82 | /** 83 | * @hidden 84 | */ 85 | constructor(page: IPageNode, name: string, title: string, type: string, layout: IVisualLayout) { 86 | this.name = name; 87 | this.title = title; 88 | this.type = type; 89 | this.layout = layout; 90 | this.page = page; 91 | } 92 | 93 | /** 94 | * Gets all visual level filters of the current visual. 95 | * 96 | * ```javascript 97 | * visual.getFilters() 98 | * .then(filters => { ... }); 99 | * ``` 100 | * 101 | * @returns {(Promise)} 102 | */ 103 | async getFilters(): Promise { 104 | try { 105 | const response = await this.page.report.service.hpm.get(`/report/pages/${this.page.name}/visuals/${this.name}/filters`, { uid: this.page.report.config.uniqueId }, this.page.report.iframe.contentWindow); 106 | return response.body; 107 | } catch (response) { 108 | throw response.body; 109 | } 110 | } 111 | 112 | /** 113 | * Update the filters for the current visual according to the operation: Add, replace all, replace by target or remove. 114 | * 115 | * ```javascript 116 | * visual.updateFilters(FiltersOperations.Add, filters) 117 | * .catch(errors => { ... }); 118 | * ``` 119 | * 120 | * @param {(IFilter[])} filters 121 | * @returns {Promise>} 122 | */ 123 | async updateFilters(operation: FiltersOperations, filters?: IFilter[]): Promise> { 124 | const updateFiltersRequest: IUpdateFiltersRequest = { 125 | filtersOperation: operation, 126 | filters: filters as VisualLevelFilters[] 127 | }; 128 | 129 | try { 130 | return await this.page.report.service.hpm.post(`/report/pages/${this.page.name}/visuals/${this.name}/filters`, updateFiltersRequest, { uid: this.page.report.config.uniqueId }, this.page.report.iframe.contentWindow); 131 | } catch (response) { 132 | throw response.body; 133 | } 134 | } 135 | 136 | /** 137 | * Removes all filters from the current visual. 138 | * 139 | * ```javascript 140 | * visual.removeFilters(); 141 | * ``` 142 | * 143 | * @returns {Promise>} 144 | */ 145 | async removeFilters(): Promise> { 146 | return await this.updateFilters(FiltersOperations.RemoveAll); 147 | } 148 | 149 | /** 150 | * Sets the filters on the current visual to 'filters'. 151 | * 152 | * ```javascript 153 | * visual.setFilters(filters); 154 | * .catch(errors => { ... }); 155 | * ``` 156 | * 157 | * @param {(IFilter[])} filters 158 | * @returns {Promise>} 159 | */ 160 | async setFilters(filters: IFilter[]): Promise> { 161 | try { 162 | return await this.page.report.service.hpm.put(`/report/pages/${this.page.name}/visuals/${this.name}/filters`, filters, { uid: this.page.report.config.uniqueId }, this.page.report.iframe.contentWindow); 163 | } catch (response) { 164 | throw response.body; 165 | } 166 | } 167 | 168 | /** 169 | * Exports Visual data. 170 | * Can export up to 30K rows. 171 | * 172 | * @param rows: Optional. Default value is 30K, maximum value is 30K as well. 173 | * @param exportDataType: Optional. Default is ExportDataType.Summarized. 174 | * ```javascript 175 | * visual.exportData() 176 | * .then(data => { ... }); 177 | * ``` 178 | * 179 | * @returns {(Promise)} 180 | */ 181 | async exportData(exportDataType?: ExportDataType, rows?: number): Promise { 182 | const exportDataRequestBody: IExportDataRequest = { 183 | rows: rows, 184 | exportDataType: exportDataType 185 | }; 186 | 187 | try { 188 | const response = await this.page.report.service.hpm.post(`/report/pages/${this.page.name}/visuals/${this.name}/exportData`, exportDataRequestBody, { uid: this.page.report.config.uniqueId }, this.page.report.iframe.contentWindow); 189 | return response.body; 190 | } catch (response) { 191 | throw response.body; 192 | } 193 | } 194 | 195 | /** 196 | * Set slicer state. 197 | * Works only for visuals of type slicer. 198 | * 199 | * @param state: A new state which contains the slicer filters. 200 | * ```javascript 201 | * visual.setSlicerState() 202 | * .then(() => { ... }); 203 | * ``` 204 | */ 205 | async setSlicerState(state: ISlicerState): Promise> { 206 | try { 207 | return await this.page.report.service.hpm.put(`/report/pages/${this.page.name}/visuals/${this.name}/slicer`, state, { uid: this.page.report.config.uniqueId }, this.page.report.iframe.contentWindow); 208 | } catch (response) { 209 | throw response.body; 210 | } 211 | } 212 | 213 | /** 214 | * Get slicer state. 215 | * Works only for visuals of type slicer. 216 | * 217 | * ```javascript 218 | * visual.getSlicerState() 219 | * .then(state => { ... }); 220 | * ``` 221 | * 222 | * @returns {(Promise)} 223 | */ 224 | async getSlicerState(): Promise { 225 | try { 226 | const response = await this.page.report.service.hpm.get(`/report/pages/${this.page.name}/visuals/${this.name}/slicer`, { uid: this.page.report.config.uniqueId }, this.page.report.iframe.contentWindow); 227 | return response.body; 228 | } catch (response) { 229 | throw response.body; 230 | } 231 | } 232 | 233 | /** 234 | * Clone existing visual to a new instance. 235 | * 236 | * @returns {(Promise)} 237 | */ 238 | async clone(request: ICloneVisualRequest = {}): Promise { 239 | try { 240 | const response = await this.page.report.service.hpm.post(`/report/pages/${this.page.name}/visuals/${this.name}/clone`, request, { uid: this.page.report.config.uniqueId }, this.page.report.iframe.contentWindow); 241 | return response.body; 242 | } catch (response) { 243 | throw response.body; 244 | } 245 | } 246 | 247 | /** 248 | * Sort a visual by dataField and direction. 249 | * 250 | * @param request: Sort by visual request. 251 | * 252 | * ```javascript 253 | * visual.sortBy(request) 254 | * .then(() => { ... }); 255 | * ``` 256 | */ 257 | async sortBy(request: ISortByVisualRequest): Promise> { 258 | try { 259 | return await this.page.report.service.hpm.put(`/report/pages/${this.page.name}/visuals/${this.name}/sortBy`, request, { uid: this.page.report.config.uniqueId }, this.page.report.iframe.contentWindow); 260 | } catch (response) { 261 | throw response.body; 262 | } 263 | } 264 | 265 | /** 266 | * Updates the position of a visual. 267 | * 268 | * ```javascript 269 | * visual.moveVisual(x, y, z) 270 | * .catch(error => { ... }); 271 | * ``` 272 | * 273 | * @param {number} x 274 | * @param {number} y 275 | * @param {number} z 276 | * @returns {Promise>} 277 | */ 278 | async moveVisual(x: number, y: number, z?: number): Promise> { 279 | const pageName = this.page.name; 280 | const visualName = this.name; 281 | const report = this.page.report as Report; 282 | return report.moveVisual(pageName, visualName, x, y, z); 283 | } 284 | 285 | /** 286 | * Updates the display state of a visual. 287 | * 288 | * ```javascript 289 | * visual.setVisualDisplayState(displayState) 290 | * .catch(error => { ... }); 291 | * ``` 292 | * 293 | * @param {VisualContainerDisplayMode} displayState 294 | * @returns {Promise>} 295 | */ 296 | async setVisualDisplayState(displayState: VisualContainerDisplayMode): Promise> { 297 | const pageName = this.page.name; 298 | const visualName = this.name; 299 | const report = this.page.report as Report; 300 | 301 | return report.setVisualDisplayState(pageName, visualName, displayState); 302 | } 303 | 304 | /** 305 | * Resize a visual. 306 | * 307 | * ```javascript 308 | * visual.resizeVisual(width, height) 309 | * .catch(error => { ... }); 310 | * ``` 311 | * 312 | * @param {number} width 313 | * @param {number} height 314 | * @returns {Promise>} 315 | */ 316 | async resizeVisual(width: number, height: number): Promise> { 317 | const pageName = this.page.name; 318 | const visualName = this.name; 319 | const report = this.page.report as Report; 320 | 321 | return report.resizeVisual(pageName, visualName, width, height); 322 | } 323 | 324 | /** 325 | * Get insights for single visual 326 | * 327 | * ```javascript 328 | * visual.getSmartNarrativeInsights(); 329 | * ``` 330 | * 331 | * @returns {Promise} 332 | */ 333 | async getSmartNarrativeInsights(): Promise { 334 | try { 335 | const response = await this.page.report.service.hpm.get(`/report/pages/${this.page.name}/visuals/${this.name}/smartNarrativeInsights`, { uid: this.page.report.config.uniqueId }, this.page.report.iframe.contentWindow); 336 | return response.body; 337 | } catch (response) { 338 | throw response.body; 339 | } 340 | } 341 | } 342 | -------------------------------------------------------------------------------- /test/SDK-to-MockApp.spec.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | import * as utils from '../src/util'; 5 | import * as service from '../src/service'; 6 | import * as embed from '../src/embed'; 7 | import * as report from '../src/report'; 8 | import * as page from '../src/page'; 9 | import * as Hpm from 'http-post-message'; 10 | import * as models from 'powerbi-models'; 11 | import * as factories from '../src/factories'; 12 | import * as util from '../src/util'; 13 | import { spyApp, setupEmbedMockApp } from './utility/mockEmbed'; 14 | import { iframeSrc } from './constsants'; 15 | 16 | describe('SDK-to-MockApp', function () { 17 | let element: HTMLDivElement; 18 | let iframe: HTMLIFrameElement; 19 | let iframeHpm: Hpm.HttpPostMessage; 20 | let powerbi: service.Service; 21 | let report: report.Report; 22 | let page1: page.Page; 23 | const embedConfiguration: embed.IEmbedConfiguration = { 24 | type: "report", 25 | id: "fakeReportIdInitialEmbed", 26 | accessToken: 'fakeTokenInitialEmbed', 27 | embedUrl: iframeSrc 28 | }; 29 | 30 | beforeEach(async function () { 31 | powerbi = new service.Service(factories.hpmFactory, factories.wpmpFactory, factories.routerFactory, { 32 | wpmpName: 'SDK-to-MockApp HostWpmp' 33 | }); 34 | 35 | spyOn(utils, 'validateEmbedUrl').and.callFake(() => { return true; }); 36 | 37 | element = document.createElement('div'); 38 | element.id = "reportContainer1"; 39 | element.className = 'powerbi-report-container2'; 40 | document.body.appendChild(element); 41 | report = powerbi.embed(element, embedConfiguration); 42 | page1 = report.page('ReportSection1'); 43 | iframe = element.getElementsByTagName('iframe')[0]; 44 | 45 | /** 46 | * Note: For testing we need to configure the eventSourceOverrideWindow to allow the host to respond to 47 | * the iframe window; however, the iframe window doesn't exist until the first embed is created. 48 | * 49 | * To work around this we create a service for the initial embed, embed a report, then set the private variable 50 | */ 51 | (powerbi.wpmp).eventSourceOverrideWindow = iframe.contentWindow; 52 | // Register Iframe side 53 | let hpmPostSpy = spyOn(powerbi.hpm, "post").and.returnValue(Promise.resolve({})); 54 | iframeHpm = setupEmbedMockApp(iframe.contentWindow, window, 'SDK-to-MockApp IframeWpmp'); 55 | 56 | await new Promise((resolve, _reject) => { 57 | iframe.addEventListener('load', () => { 58 | resolve(null); 59 | }); 60 | }); 61 | 62 | hpmPostSpy.and.callThrough(); 63 | }); 64 | 65 | afterEach(function () { 66 | powerbi.reset(element); 67 | element.remove(); 68 | powerbi.wpmp?.stop(); 69 | spyApp.reset(); 70 | }); 71 | 72 | describe('report', function () { 73 | beforeEach(function () { 74 | spyOn(utils, "getTimeDiffInMilliseconds").and.callFake(() => 700); // Prevent requests from being throttled. 75 | }); 76 | 77 | describe('load', function () { 78 | it('report.load() returns promise that resolves with null if the report load successful', async function () { 79 | try { 80 | const response = await report.load(undefined); 81 | // Assert 82 | expect(response).toEqual({} as any); 83 | } catch (error) { 84 | fail("lod shouldn't fail"); 85 | } 86 | }); 87 | }); 88 | 89 | describe('pages', function () { 90 | it('report.getPages() return promise that rejects with server error if there was error getting pages', async function () { 91 | // Arrange 92 | const testData = { 93 | expectedError: { 94 | message: 'internal server error' 95 | } 96 | }; 97 | 98 | try { 99 | spyApp.getPages.and.callFake(() => Promise.reject(testData.expectedError)); 100 | // Act 101 | await report.getPages(); 102 | fail("getPagesshouldn't succeed"); 103 | } catch (error) { 104 | // Assert 105 | expect(spyApp.getPages).toHaveBeenCalled(); 106 | expect(error).toEqual(jasmine.objectContaining(testData.expectedError)); 107 | } 108 | }); 109 | 110 | it('report.getPages() returns promise that resolves with list of page names', async function () { 111 | // Arrange 112 | const testData = { 113 | pages: [ 114 | { 115 | name: "page1", 116 | displayName: "Page 1", 117 | isActive: true 118 | } 119 | ] 120 | }; 121 | 122 | try { 123 | spyApp.getPages.and.returnValue(Promise.resolve(testData.pages)); 124 | const pages = await report.getPages(); 125 | // Assert 126 | expect(spyApp.getPages).toHaveBeenCalled(); 127 | // Workaround to compare pages 128 | pages.forEach(page => { 129 | const testPage = util.find(p => p.name === page.name, testData.pages); 130 | if (testPage) { 131 | expect(page.name).toEqual(testPage.name); 132 | expect(page.isActive).toEqual(testPage.isActive); 133 | } 134 | else { 135 | expect(true).toBe(false); 136 | } 137 | }); 138 | } catch (error) { 139 | console.log("getPages failed with", error); 140 | fail("getPages failed"); 141 | } 142 | }); 143 | }); 144 | 145 | describe('filters', function () { 146 | it('report.getFilters() returns promise that rejects with server error if there was problem getting filters', async function () { 147 | // Arrange 148 | const testData = { 149 | expectedError: { 150 | message: 'could not serialize filters' 151 | } 152 | }; 153 | 154 | try { 155 | spyApp.getFilters.and.callFake(() => Promise.reject(testData.expectedError)); 156 | await report.getFilters(); 157 | fail("getFilters shouldn't succeed"); 158 | } catch (error) { 159 | // Assert 160 | expect(spyApp.getFilters).toHaveBeenCalled(); 161 | expect(error).toEqual(jasmine.objectContaining(testData.expectedError)); 162 | } 163 | }); 164 | 165 | it('report.getFilters() returns promise that resolves with filters is request is successful', async function () { 166 | // Arrange 167 | const testData = { 168 | filters: [ 169 | { x: 'fakeFilter' } 170 | ] 171 | }; 172 | 173 | spyApp.getFilters.and.returnValue(Promise.resolve(testData.filters)); 174 | try { 175 | 176 | // Act 177 | const filters = await report.getFilters(); 178 | // Assert 179 | expect(spyApp.getFilters).toHaveBeenCalled(); 180 | // @ts-ignore as testData is not of type IFilter 181 | expect(filters).toEqual(testData.filters); 182 | } catch (error) { 183 | fail("get filtershousln't fails"); 184 | } 185 | }); 186 | 187 | it('report.setFilters(filters) returns promise that rejects with validation errors if filter is invalid', async function () { 188 | // Arrange 189 | const testData = { 190 | filters: [ 191 | (new models.BasicFilter({ table: "cars", column: "make" }, "In", ["subaru", "honda"])).toJSON() 192 | ], 193 | expectedErrors: [ 194 | { 195 | message: 'invalid filter' 196 | } 197 | ] 198 | }; 199 | 200 | spyApp.validateFilter.and.callFake(() => Promise.reject(testData.expectedErrors)); 201 | try { 202 | 203 | // Act 204 | await report.setFilters(testData.filters); 205 | fail("et filter should fail"); 206 | } catch (error) { 207 | // Assert 208 | expect(spyApp.validateFilter).toHaveBeenCalledWith(testData.filters[0]); 209 | expect(spyApp.setFilters).not.toHaveBeenCalled(); 210 | expect(error).toEqual(jasmine.objectContaining(testData.expectedErrors)); 211 | } 212 | }); 213 | 214 | it('report.setFilters(filters) returns promise that resolves with null if filter was valid and request is accepted', async function () { 215 | // Arrange 216 | const testData = { 217 | filters: [(new models.BasicFilter({ table: "cars", column: "make" }, "In", ["subaru", "honda"])).toJSON()] 218 | }; 219 | 220 | spyApp.validateFilter.and.returnValue(Promise.resolve(null)); 221 | spyApp.setFilters.and.returnValue(Promise.resolve(null)); 222 | try { 223 | // Act 224 | await report.setFilters(testData.filters); 225 | expect(spyApp.validateFilter).toHaveBeenCalledWith(testData.filters[0]); 226 | expect(spyApp.setFilters).toHaveBeenCalledWith(testData.filters); 227 | } catch (error) { 228 | fail("why fail"); 229 | } 230 | }); 231 | 232 | it('report.removeFilters() returns promise that resolves with null if the request was accepted', async function () { 233 | // Arrange 234 | let spy = spyOn(report, 'updateFilters').and.callFake(() => Promise.resolve(null)); 235 | try { 236 | // Act 237 | await report.removeFilters(); 238 | // Assert 239 | expect(spy).toHaveBeenCalledWith(models.FiltersOperations.RemoveAll); 240 | } catch (error) { 241 | fail("remove fialter shouldn't fail"); 242 | } 243 | }); 244 | }); 245 | 246 | describe('print', function () { 247 | it('report.print() returns promise that resolves with null if the report print command was accepted', async function () { 248 | // Arrange 249 | spyApp.print.and.returnValue(Promise.resolve({})); 250 | // Act 251 | const response = await report.print(); 252 | // Assert 253 | expect(spyApp.print).toHaveBeenCalled(); 254 | expect(response).toEqual(); 255 | }); 256 | }); 257 | 258 | describe('refresh', function () { 259 | it('report.refresh() returns promise that resolves with null if the report refresh command was accepted', async function () { 260 | // Arrange 261 | spyApp.refreshData.and.returnValue(Promise.resolve(null)); 262 | // Act 263 | const response = await report.refresh(); 264 | // Assert 265 | expect(spyApp.refreshData).toHaveBeenCalled(); 266 | expect(response).toEqual(undefined); 267 | }); 268 | }); 269 | 270 | describe('settings', function () { 271 | it('report.updateSettings(setting) returns promise that rejects with validation error if object is invalid', async function () { 272 | // Arrange 273 | const testData = { 274 | settings: { 275 | filterPaneEnabled: false 276 | }, 277 | expectedErrors: [ 278 | { 279 | message: 'invalid target' 280 | } 281 | ] 282 | }; 283 | spyApp.validateSettings.and.callFake(() => Promise.reject(testData.expectedErrors)); 284 | 285 | try { 286 | // Act 287 | await report.updateSettings(testData.settings); 288 | fail("shouldfail"); 289 | } catch (errors) { 290 | // Assert 291 | expect(spyApp.validateSettings).toHaveBeenCalledWith(testData.settings); 292 | expect(spyApp.updateSettings).not.toHaveBeenCalled(); 293 | expect(errors).toEqual(jasmine.objectContaining(testData.expectedErrors)); 294 | } 295 | }); 296 | 297 | it('report.updateSettings(settings) returns promise that resolves with null if requst is valid and accepted', async function () { 298 | // Arrange 299 | const testData = { 300 | settings: { 301 | filterPaneEnabled: false 302 | }, 303 | expectedErrors: [ 304 | { 305 | message: 'invalid target' 306 | } 307 | ] 308 | }; 309 | 310 | try { 311 | spyApp.validateSettings.and.returnValue(Promise.resolve(null)); 312 | spyApp.updateSettings.and.returnValue(Promise.resolve(null)); 313 | // Act 314 | await report.updateSettings(testData.settings); 315 | // Assert 316 | expect(spyApp.validateSettings).toHaveBeenCalledWith(testData.settings); 317 | expect(spyApp.updateSettings).toHaveBeenCalledWith(testData.settings); 318 | } catch (error) { 319 | console.log("updateSettings failed with", error); 320 | fail("updateSettings failed"); 321 | } 322 | }); 323 | }); 324 | 325 | describe('page', function () { 326 | describe('filters', function () { 327 | 328 | beforeEach(() => { 329 | spyApp.validatePage.and.returnValue(Promise.resolve(null)); 330 | }); 331 | 332 | it('page.getFilters() returns promise that rejects with server error if there was problem getting filters', async function () { 333 | // Arrange 334 | const testData = { 335 | expectedError: { 336 | message: 'could not serialize filters' 337 | } 338 | }; 339 | 340 | try { 341 | spyApp.getFilters.and.callFake(() => Promise.reject(testData.expectedError)); 342 | 343 | await page1.getFilters(); 344 | } catch (error) { 345 | // Assert 346 | expect(spyApp.getFilters).toHaveBeenCalled(); 347 | expect(error).toEqual(jasmine.objectContaining(testData.expectedError)); 348 | 349 | } 350 | }); 351 | 352 | it('page.getFilters() returns promise that resolves with filters is request is successful', async function () { 353 | // Arrange 354 | const testData = { 355 | filters: [ 356 | { x: 'fakeFilter' } 357 | ] 358 | }; 359 | 360 | try { 361 | 362 | spyApp.getFilters.and.returnValue(Promise.resolve(testData.filters)); 363 | const filters = await page1.getFilters(); 364 | // Assert 365 | expect(spyApp.getFilters).toHaveBeenCalled(); 366 | // @ts-ignore as testData is not of type IFilter as testData is not of type IFilter 367 | expect(filters).toEqual(testData.filters); 368 | } catch (error) { 369 | fail("getFilters shouldn't fail"); 370 | } 371 | }); 372 | 373 | it('page.setFilters(filters) returns promise that rejects with validation errors if filter is invalid', async function () { 374 | // Arrange 375 | const testData = { 376 | filters: [ 377 | (new models.BasicFilter({ table: "cars", column: "make" }, "In", ["subaru", "honda"])).toJSON() 378 | ], 379 | expectedErrors: [ 380 | { 381 | message: 'invalid filter' 382 | } 383 | ] 384 | }; 385 | 386 | // await iframeLoaded; 387 | try { 388 | spyApp.validateFilter.and.callFake(() => Promise.reject(testData.expectedErrors)); 389 | await page1.setFilters(testData.filters); 390 | fail("setilters shouldn't fail"); 391 | } catch (error) { 392 | expect(spyApp.validateFilter).toHaveBeenCalledWith(testData.filters[0]); 393 | expect(spyApp.setFilters).not.toHaveBeenCalled(); 394 | expect(error).toEqual(jasmine.objectContaining(testData.expectedErrors)); 395 | } 396 | }); 397 | 398 | it('page.setFilters(filters) returns promise that resolves with null if filter was valid and request is accepted', async function () { 399 | // Arrange 400 | const testData = { 401 | filters: [(new models.BasicFilter({ table: "cars", column: "make" }, "In", ["subaru", "honda"])).toJSON()] 402 | }; 403 | 404 | spyApp.validateFilter.and.returnValue(Promise.resolve(null)); 405 | spyApp.setFilters.and.returnValue(Promise.resolve(null)); 406 | try { 407 | await page1.setFilters(testData.filters); 408 | expect(spyApp.validateFilter).toHaveBeenCalledWith(testData.filters[0]); 409 | expect(spyApp.setFilters).toHaveBeenCalledWith(testData.filters); 410 | } catch (error) { 411 | console.log("setFilters failed with", error); 412 | fail("setilters failed"); 413 | } 414 | }); 415 | 416 | it('page.removeFilters() returns promise that resolves with null if the request was accepted', async function () { 417 | // Arrange 418 | try { 419 | spyApp.updateFilters.and.returnValue(Promise.resolve(null)); 420 | // Act 421 | await page1.removeFilters(); 422 | } catch (error) { 423 | console.log("removeFilters failed with", error); 424 | fail("removeFilters failed"); 425 | } 426 | // Assert 427 | expect(spyApp.updateFilters).toHaveBeenCalledWith(models.FiltersOperations.RemoveAll, undefined); 428 | }); 429 | 430 | describe('setActive', function () { 431 | it('page.setActive() returns promise that rejects if page is invalid', async function () { 432 | // Arrange 433 | const testData = { 434 | errors: [ 435 | { 436 | message: 'page xyz was not found in report' 437 | } 438 | ] 439 | }; 440 | 441 | spyApp.validatePage.and.callFake(() => Promise.reject(testData.errors)); 442 | try { 443 | // Act 444 | await page1.setActive(); 445 | fail("setActive shouldn't succeed"); 446 | 447 | } catch (errors) { 448 | expect(spyApp.validatePage).toHaveBeenCalled(); 449 | expect(spyApp.setPage).not.toHaveBeenCalled(); 450 | expect(errors).toEqual(jasmine.objectContaining(testData.errors)); 451 | } 452 | spyApp.validatePage.and.callThrough(); 453 | }); 454 | 455 | it('page.setActive() returns promise that resolves with null if request is successful', async function () { 456 | // Act 457 | spyApp.validatePage.and.returnValue(Promise.resolve(null)); 458 | spyApp.setPage.and.returnValue(Promise.resolve(null)); 459 | try { 460 | await page1.setActive(); 461 | expect(spyApp.validatePage).toHaveBeenCalled(); 462 | expect(spyApp.setPage).toHaveBeenCalled(); 463 | } catch (error) { 464 | console.log("setActive failed with ", error); 465 | fail("setActive failed"); 466 | } 467 | }); 468 | }); 469 | }); 470 | }); 471 | 472 | describe('SDK-to-Router (Event subscription)', function () { 473 | it(`report.on(eventName, handler) should throw error if eventName is not supported`, function () { 474 | // Arrange 475 | const testData = { 476 | eventName: 'xyz', 477 | handler: jasmine.createSpy('handler') 478 | }; 479 | 480 | try { 481 | report.on(testData.eventName, testData.handler); 482 | fail("should throw exception"); 483 | } catch (error) { 484 | expect(1).toEqual(1); 485 | } 486 | }); 487 | 488 | it(`report.on(eventName, handler) should register handler and be called when POST /report/:uniqueId/events/:eventName is received`, async function () { 489 | // Arrange 490 | const testData = { 491 | reportId: 'fakeReportId', 492 | eventName: 'pageChanged', 493 | handler: jasmine.createSpy('handler'), 494 | simulatedPageChangeBody: { 495 | initiator: 'sdk', 496 | newPage: { 497 | name: 'page1', 498 | displayName: 'Page 1' 499 | } 500 | }, 501 | expectedEvent: { 502 | detail: { 503 | initiator: 'sdk', 504 | newPage: report.page('page1') 505 | } 506 | } 507 | }; 508 | 509 | report.on(testData.eventName, testData.handler); 510 | try { 511 | // Act 512 | await iframeHpm.post(`/reports/${report.config.uniqueId}/events/${testData.eventName}`, testData.simulatedPageChangeBody); 513 | 514 | } catch (error) { 515 | fail("testshouldn't fail"); 516 | } 517 | // Assert 518 | expect(testData.handler).toHaveBeenCalledWith(jasmine.any(CustomEvent)); 519 | // Workaround to compare pages which prevents recursive loop in jasmine equals 520 | // expect(testData.handler2).toHaveBeenCalledWith(jasmine.objectContaining({ detail: testData.simulatedPageChangeBody })); 521 | expect(testData.handler.calls.mostRecent().args[0].detail.newPage.name).toEqual(testData.expectedEvent.detail.newPage.name); 522 | }); 523 | 524 | it(`if multiple reports with the same id are loaded into the host, and event occurs on one of them, only one report handler should be called`, async function () { 525 | // Arrange 526 | const testData = { 527 | reportId: 'fakeReportId', 528 | eventName: 'pageChanged', 529 | handler: jasmine.createSpy('handler'), 530 | handler2: jasmine.createSpy('handler2'), 531 | simulatedPageChangeBody: { 532 | initiator: 'sdk', 533 | newPage: { 534 | name: 'page1', 535 | displayName: 'Page 1' 536 | } 537 | } 538 | }; 539 | 540 | // Create a second iframe and report 541 | const element2 = document.createElement('div'); 542 | element2.id = "reportContainer2"; 543 | element2.className = 'powerbi-report-container3'; 544 | document.body.appendChild(element2); 545 | const report2 = powerbi.embed(element2, embedConfiguration); 546 | const iframe2 = element2.getElementsByTagName('iframe')[0]; 547 | setupEmbedMockApp(iframe2.contentWindow, window, 'SDK-to-MockApp IframeWpmp2'); 548 | await new Promise((resolve, _reject) => { 549 | iframe2.addEventListener('load', () => { 550 | resolve(null); 551 | }); 552 | }); 553 | 554 | report.on(testData.eventName, testData.handler); 555 | report2.on(testData.eventName, testData.handler2); 556 | 557 | try { 558 | await iframeHpm.post(`/reports/${report2.config.uniqueId}/events/${testData.eventName}`, testData.simulatedPageChangeBody); 559 | } catch (error) { 560 | powerbi.reset(element2); 561 | element2.remove(); 562 | fail("hpm post shouldn't fail"); 563 | } 564 | // Act 565 | expect(testData.handler).not.toHaveBeenCalled(); 566 | expect(testData.handler2).toHaveBeenCalledWith(jasmine.any(CustomEvent)); 567 | // Workaround to compare pages which prevents recursive loop in jasmine equals 568 | // expect(testData.handler).toHaveBeenCalledWith(jasmine.objectContaining(testData.expectedEvent)); 569 | expect(testData.handler2.calls.mostRecent().args[0].detail.newPage.name).toEqual(testData.simulatedPageChangeBody.newPage.name); 570 | powerbi.reset(element2); 571 | element2.remove(); 572 | }); 573 | 574 | it(`ensure load event is allowed`, async function () { 575 | // Arrange 576 | const testData = { 577 | reportId: 'fakeReportId', 578 | eventName: 'loaded', 579 | handler: jasmine.createSpy('handler3'), 580 | simulatedBody: { 581 | initiator: 'sdk' 582 | } 583 | }; 584 | 585 | report.on(testData.eventName, testData.handler); 586 | 587 | // Act 588 | try { 589 | await iframeHpm.post(`/reports/${report.config.uniqueId}/events/${testData.eventName}`, testData.simulatedBody); 590 | } catch (error) { 591 | fail("ensure load event is allowed failed"); 592 | } 593 | 594 | // Assert 595 | expect(testData.handler).toHaveBeenCalledWith(jasmine.any(CustomEvent)); 596 | expect(testData.handler).toHaveBeenCalledWith(jasmine.objectContaining({ detail: testData.simulatedBody })); 597 | }); 598 | }); 599 | }); 600 | }); 601 | -------------------------------------------------------------------------------- /test/SDK-to-WPMP.spec.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | import * as service from '../src/service'; 5 | import * as report from '../src/report'; 6 | import * as Wpmp from 'window-post-message-proxy'; 7 | import * as factories from '../src/factories'; 8 | import * as utils from '../src/util'; 9 | import { spyWpmp } from './utility/mockWpmp'; 10 | import { spyHpm } from './utility/mockHpm'; 11 | import { spyRouter } from './utility/mockRouter'; 12 | import { iframeSrc } from './constsants'; 13 | 14 | describe('SDK-to-WPMP', function () { 15 | let element: HTMLDivElement; 16 | let powerbi: service.Service; 17 | let report: report.Report; 18 | let uniqueId: string; 19 | 20 | beforeEach(function () { 21 | spyOn(utils, 'validateEmbedUrl').and.callFake(() => { return true; }); 22 | const spyWpmpFactory: factories.IWpmpFactory = (_name?: string, _logMessages?: boolean) => { 23 | return spyWpmp; 24 | }; 25 | 26 | powerbi = new service.Service(factories.hpmFactory, spyWpmpFactory, factories.routerFactory); 27 | 28 | element = document.createElement('div'); 29 | element.className = 'powerbi-report-container'; 30 | 31 | const embedConfiguration = { 32 | type: "report", 33 | id: "fakeReportId", 34 | accessToken: 'fakeToken', 35 | embedUrl: iframeSrc, 36 | wpmpName: 'SDK-to-WPMP report wpmp' 37 | }; 38 | const hpmPostpy = spyOn(powerbi.hpm, "post").and.callFake(() => Promise.resolve({})); 39 | report = powerbi.embed(element, embedConfiguration); 40 | hpmPostpy.and.callThrough(); 41 | uniqueId = report.config.uniqueId; 42 | spyHpm.post.calls.reset(); 43 | }); 44 | 45 | afterEach(function () { 46 | powerbi.reset(element); 47 | element.remove(); 48 | 49 | spyWpmp.stop(); 50 | spyWpmp.addHandler.calls.reset(); 51 | spyWpmp.clearHandlers(); 52 | 53 | spyHpm.get.calls.reset(); 54 | spyHpm.post.calls.reset(); 55 | spyHpm.patch.calls.reset(); 56 | spyHpm.put.calls.reset(); 57 | spyHpm.delete.calls.reset(); 58 | 59 | spyRouter.get.calls.reset(); 60 | spyRouter.post.calls.reset(); 61 | spyRouter.patch.calls.reset(); 62 | spyRouter.put.calls.reset(); 63 | spyRouter.delete.calls.reset(); 64 | }); 65 | 66 | describe('Event handlers', function () { 67 | it(`handler passed to report.on(eventName, handler) is called when POST /report/:uniqueId/events/:eventName is received`, function () { 68 | // Arrange 69 | const testData = { 70 | eventName: 'filtersApplied', 71 | handler: jasmine.createSpy('handler'), 72 | filtersAppliedEvent: { 73 | data: { 74 | method: 'POST', 75 | url: `/reports/${uniqueId}/events/filtersApplied`, 76 | body: { 77 | initiator: 'sdk', 78 | filters: [ 79 | { 80 | x: 'fakeFilter' 81 | } 82 | ] 83 | } 84 | } 85 | } 86 | }; 87 | 88 | report.on(testData.eventName, testData.handler); 89 | 90 | // Act 91 | spyWpmp.onMessageReceived(testData.filtersAppliedEvent); 92 | 93 | // Assert 94 | expect(testData.handler).toHaveBeenCalledWith(jasmine.objectContaining({ detail: testData.filtersAppliedEvent.data.body })); 95 | }); 96 | 97 | it(`off('eventName', handler) will remove single handler which matches function reference for that event`, function () { 98 | // Arrange 99 | const testData = { 100 | eventName: 'filtersApplied', 101 | handler: jasmine.createSpy('handler1'), 102 | simulatedEvent: { 103 | data: { 104 | method: 'POST', 105 | url: `/reports/${uniqueId}/events/filtersApplied`, 106 | body: { 107 | initiator: 'sdk', 108 | filter: { 109 | x: '1', 110 | y: '2' 111 | } 112 | } 113 | } 114 | } 115 | }; 116 | 117 | report.on(testData.eventName, testData.handler); 118 | report.off(testData.eventName, testData.handler); 119 | 120 | // Act 121 | spyWpmp.onMessageReceived(testData.simulatedEvent); 122 | 123 | // Assert 124 | expect(testData.handler).not.toHaveBeenCalled(); 125 | }); 126 | 127 | it('if multiple handlers for the same event are registered they will all be called', function () { 128 | // Arrange 129 | const testData = { 130 | eventName: 'filtersApplied', 131 | handler: jasmine.createSpy('handler1'), 132 | handler2: jasmine.createSpy('handler2'), 133 | handler3: jasmine.createSpy('handler3'), 134 | simulatedEvent: { 135 | data: { 136 | method: 'POST', 137 | url: `/reports/${uniqueId}/events/filtersApplied`, 138 | body: { 139 | initiator: 'sdk', 140 | filter: { 141 | x: '1', 142 | y: '2' 143 | } 144 | } 145 | } 146 | } 147 | }; 148 | 149 | report.on(testData.eventName, testData.handler); 150 | report.on(testData.eventName, testData.handler2); 151 | report.on(testData.eventName, testData.handler3); 152 | 153 | // Act 154 | spyWpmp.onMessageReceived(testData.simulatedEvent); 155 | 156 | // Assert 157 | expect(testData.handler).toHaveBeenCalledWith(jasmine.objectContaining({ detail: testData.simulatedEvent.data.body })); 158 | expect(testData.handler2).toHaveBeenCalledWith(jasmine.objectContaining({ detail: testData.simulatedEvent.data.body })); 159 | expect(testData.handler3).toHaveBeenCalledWith(jasmine.objectContaining({ detail: testData.simulatedEvent.data.body })); 160 | }); 161 | 162 | it(`off('eventName') will remove all handlers which matches event name`, function () { 163 | // Arrange 164 | const testData = { 165 | eventName: 'filtersApplied', 166 | handler: jasmine.createSpy('handler1'), 167 | handler2: jasmine.createSpy('handler2'), 168 | handler3: jasmine.createSpy('handler3'), 169 | simulatedEvent: { 170 | data: { 171 | method: 'POST', 172 | url: '/reports/fakeReportId/events/filtersApplied', 173 | body: { 174 | initiator: 'sdk', 175 | filter: { 176 | x: '1', 177 | y: '2' 178 | } 179 | } 180 | } 181 | } 182 | }; 183 | 184 | report.on(testData.eventName, testData.handler); 185 | report.on(testData.eventName, testData.handler2); 186 | report.on(testData.eventName, testData.handler3); 187 | report.off(testData.eventName); 188 | 189 | // Act 190 | spyWpmp.onMessageReceived(testData.simulatedEvent); 191 | 192 | // Assert 193 | expect(testData.handler).not.toHaveBeenCalled(); 194 | expect(testData.handler2).not.toHaveBeenCalled(); 195 | expect(testData.handler3).not.toHaveBeenCalled(); 196 | }); 197 | }); 198 | }); 199 | -------------------------------------------------------------------------------- /test/constsants.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | declare global { 5 | interface Window { 6 | __karma__: any; 7 | } 8 | } 9 | 10 | export const iframeSrc = "base/test/utility/noop.html"; 11 | window.onbeforeunload = null; 12 | 13 | -------------------------------------------------------------------------------- /test/test.spec.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | import * as service from '../src/service'; 5 | import * as factories from '../src/factories'; 6 | import * as utils from '../src/util'; 7 | 8 | // Avoid adding new tests to this file, create another spec file instead. 9 | 10 | describe('embed', function () { 11 | let powerbi: service.Service; 12 | let container: HTMLDivElement; 13 | let iframe: HTMLIFrameElement; 14 | 15 | beforeEach(function () { 16 | spyOn(utils, 'validateEmbedUrl').and.callFake(() => { return true; }); 17 | powerbi = new service.Service(factories.hpmFactory, factories.wpmpFactory, factories.routerFactory); 18 | powerbi.accessToken = 'ABC123'; 19 | container = document.createElement('iframe'); 20 | container.setAttribute("powerbi-embed-url", "https://app.powerbi.com/reportEmbed?reportId=ABC123"); 21 | container.setAttribute("powerbi-type", "report"); 22 | document.body.appendChild(container); 23 | 24 | powerbi.embed(container); 25 | iframe = container.getElementsByTagName('iframe')[0]; 26 | }); 27 | 28 | afterEach(function () { 29 | powerbi.reset(container); 30 | container.remove(); 31 | powerbi.wpmp.stop(); 32 | }); 33 | 34 | describe('iframe', function () { 35 | it('has a src', function () { 36 | expect(iframe.src.length).toBeGreaterThan(0); 37 | }); 38 | 39 | it('disables scrollbars by default', function () { 40 | expect(iframe.getAttribute('scrolling')).toEqual('no'); 41 | }); 42 | 43 | it('sets width/height to 100%', function () { 44 | expect(iframe.style.width).toEqual('100%'); 45 | expect(iframe.style.height).toEqual('100%'); 46 | }); 47 | }); 48 | 49 | describe('fullscreen', function () { 50 | it('sets the iframe as the fullscreen element', function () { 51 | let requestFullscreenSpy = jasmine.createSpy(); 52 | iframe.requestFullscreen = requestFullscreenSpy; 53 | let report = powerbi.get(container); 54 | report.fullscreen(); 55 | 56 | expect(requestFullscreenSpy).toHaveBeenCalled(); 57 | }); 58 | }); 59 | 60 | describe('exitFullscreen', function () { 61 | it('clears the iframe fullscreen element', function () { 62 | let requestFullscreenSpy = jasmine.createSpy(); 63 | iframe.requestFullscreen = requestFullscreenSpy; 64 | let report = powerbi.get(container); 65 | report.fullscreen(); 66 | report.exitFullscreen(); 67 | expect(requestFullscreenSpy).toHaveBeenCalled(); 68 | }); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /test/utility/mockApp.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | import * as models from 'powerbi-models'; 5 | 6 | export interface IApp { 7 | // Load 8 | dashboardLoad(config: models.IDashboardLoadConfiguration): Promise; 9 | validateDashboardLoad(config: models.IDashboardLoadConfiguration): Promise; 10 | reportLoad(config: models.IReportLoadConfiguration): Promise; 11 | validateReportLoad(config: models.IReportLoadConfiguration): Promise; 12 | render(): Promise; 13 | // Settings 14 | updateSettings(settings: models.ISettings): Promise; 15 | validateSettings(settigns: models.ISettings): Promise; 16 | addContextMenuCommand(commandName: string, commandTitle: string, contextMenuTitle: string, menuLocation?: string, visualName?: string, visualType?: string, groupName?: string): Promise; 17 | addOptionsMenuCommand(commandName: string, commandTitle: string, optionsMenuTitle: string, menuLocation?: string, visualName?: string, visualType?: string, groupName?: string, commandIcon?: string): Promise; 18 | removeContextMenuCommand(commandName: string): Promise; 19 | removeOptionsMenuCommand(commandName: string): Promise; 20 | setVisualDisplayState(pageName: string, visualName: string, displayState: models.VisualContainerDisplayMode): Promise; 21 | resizeVisual(pageName: string, visualName: string, width: number, height: number): Promise; 22 | resizeActivePage(pageSizeType: models.PageSizeType, width: number, height: number): Promise; 23 | moveVisual(pageName: string, visualName: string, x: number, y: number, z?: number): Promise; 24 | // Pages 25 | getPages(): Promise; 26 | getPageByName(pageName: string): Promise; 27 | getActivePage(): Promise; 28 | setPage(pageName: string): Promise; 29 | validatePage(page: models.IPage): Promise; 30 | // Visuals 31 | validateVisual(page: models.IPage, visual: models.IVisual): Promise; 32 | getVisualByName(visualName: string): Promise; 33 | // Filters 34 | getFilters(): Promise; 35 | updateFilters(operation: models.FiltersOperations, filters: models.IFilter[]): Promise; 36 | setFilters(filters: models.IFilter[]): Promise; 37 | validateFilter(filter: models.IFilter): Promise; 38 | // Other 39 | print(): Promise; 40 | refreshData(): Promise; 41 | exportData(): Promise; 42 | validateCreateReport(config: models.IReportCreateConfiguration): Promise; 43 | validateQuickCreate(config: models.IQuickCreateConfiguration): Promise; 44 | switchMode(): Promise; 45 | save(): Promise; 46 | saveAs(saveAsParameters: models.ISaveAsParameters): Promise; 47 | setAccessToken(accessToken: string): Promise; 48 | switchLayout(layoutType: models.LayoutType): Promise; 49 | } 50 | 51 | export const mockAppSpyObj = { 52 | // Load 53 | dashboardLoad: jasmine.createSpy("dashboardLoad").and.returnValue(Promise.resolve(null)), 54 | validateDashboardLoad: jasmine.createSpy("validateDashboardLoad").and.callFake(models.validateDashboardLoad), 55 | reportLoad: jasmine.createSpy("reportLoad").and.returnValue(Promise.resolve(null)), 56 | validateReportLoad: jasmine.createSpy("validateReportLoad").and.callFake(models.validateReportLoad), 57 | render: jasmine.createSpy("render").and.returnValue(Promise.resolve(null)), 58 | // Settings 59 | updateSettings: jasmine.createSpy("updateSettings").and.returnValue(Promise.resolve(null)), 60 | validateSettings: jasmine.createSpy("validateSettings").and.callFake(models.validateSettings), 61 | addContextMenuCommand: jasmine.createSpy("addContextMenuCommand").and.returnValue(Promise.resolve(null)), 62 | addOptionsMenuCommand: jasmine.createSpy("addOptionsMenuCommand").and.returnValue(Promise.resolve(null)), 63 | removeContextMenuCommand: jasmine.createSpy("removeContextMenuCommand").and.returnValue(Promise.resolve(null)), 64 | removeOptionsMenuCommand: jasmine.createSpy("removeOptionsMenuCommand").and.returnValue(Promise.resolve(null)), 65 | setVisualDisplayState: jasmine.createSpy("setVisualDisplayState").and.returnValue(Promise.resolve(null)), 66 | resizeVisual: jasmine.createSpy("resizeVisual").and.returnValue(Promise.resolve(null)), 67 | resizeActivePage: jasmine.createSpy("resizeActivePage").and.returnValue(Promise.resolve(null)), 68 | moveVisual: jasmine.createSpy("moveVisual").and.returnValue(Promise.resolve(null)), 69 | // Pages 70 | getPages: jasmine.createSpy("getPages").and.returnValue(Promise.resolve(null)), 71 | getPageByName: jasmine.createSpy("getPageByName").and.returnValue(Promise.resolve(null)), 72 | getActivePage: jasmine.createSpy("getActivePage").and.returnValue(Promise.resolve(null)), 73 | setPage: jasmine.createSpy("setPage").and.returnValue(Promise.resolve(null)), 74 | validatePage: jasmine.createSpy("validatePage").and.returnValue(Promise.resolve(null)), 75 | // Visuals 76 | validateVisual: jasmine.createSpy("validateVisual").and.returnValue(Promise.resolve(null)), 77 | getVisualByName: jasmine.createSpy("getVisualByName").and.returnValue(Promise.resolve(null)), 78 | // Filters 79 | getFilters: jasmine.createSpy("getFilters").and.returnValue(Promise.resolve(null)), 80 | updateFilters: jasmine.createSpy("updateFilters").and.returnValue(Promise.resolve(null)), 81 | setFilters: jasmine.createSpy("setFilters").and.returnValue(Promise.resolve(null)), 82 | validateFilter: jasmine.createSpy("validateFilter").and.callFake(models.validateFilter), 83 | // Other 84 | print: jasmine.createSpy("print").and.returnValue(Promise.resolve(null)), 85 | refreshData: jasmine.createSpy("refreshData").and.returnValue(Promise.resolve(null)), 86 | exportData: jasmine.createSpy("exportData").and.returnValue(Promise.resolve(null)), 87 | validateCreateReport: jasmine.createSpy("validateCreateReport").and.callFake(models.validateCreateReport), 88 | validateQuickCreate: jasmine.createSpy("validateQuickCreate").and.callFake(models.validateQuickCreate), 89 | switchMode: jasmine.createSpy("switchMode").and.returnValue(Promise.resolve(null)), 90 | save: jasmine.createSpy("save").and.returnValue(Promise.resolve(null)), 91 | saveAs: jasmine.createSpy("saveAs").and.returnValue(Promise.resolve(null)), 92 | setAccessToken: jasmine.createSpy("setAccessToken").and.returnValue(Promise.resolve(null)), 93 | switchLayout: jasmine.createSpy("switchLayout").and.returnValue(Promise.resolve(null)), 94 | 95 | reset(): void { 96 | mockAppSpyObj.dashboardLoad.calls.reset(); 97 | mockAppSpyObj.dashboardLoad.and.callThrough(); 98 | mockAppSpyObj.validateDashboardLoad.calls.reset(); 99 | mockAppSpyObj.validateDashboardLoad.and.callThrough(); 100 | mockAppSpyObj.reportLoad.calls.reset(); 101 | mockAppSpyObj.reportLoad.and.callThrough(); 102 | mockAppSpyObj.render.calls.reset(); 103 | mockAppSpyObj.render.and.callThrough(); 104 | mockAppSpyObj.validateReportLoad.calls.reset(); 105 | mockAppSpyObj.validateReportLoad.and.callThrough(); 106 | mockAppSpyObj.updateSettings.calls.reset(); 107 | mockAppSpyObj.updateSettings.and.callThrough(); 108 | mockAppSpyObj.validateSettings.calls.reset(); 109 | mockAppSpyObj.validateSettings.and.callThrough(); 110 | mockAppSpyObj.setVisualDisplayState.calls.reset(); 111 | mockAppSpyObj.setVisualDisplayState.and.callThrough(); 112 | mockAppSpyObj.resizeVisual.calls.reset(); 113 | mockAppSpyObj.resizeVisual.and.callThrough(); 114 | mockAppSpyObj.resizeActivePage.calls.reset(); 115 | mockAppSpyObj.resizeActivePage.and.callThrough(); 116 | mockAppSpyObj.moveVisual.calls.reset(); 117 | mockAppSpyObj.moveVisual.and.callThrough(); 118 | mockAppSpyObj.getPages.calls.reset(); 119 | mockAppSpyObj.getPages.and.callThrough(); 120 | mockAppSpyObj.getPageByName.calls.reset(); 121 | mockAppSpyObj.getPageByName.and.callThrough(); 122 | mockAppSpyObj.getActivePage.calls.reset(); 123 | mockAppSpyObj.getActivePage.and.callThrough(); 124 | mockAppSpyObj.setPage.calls.reset(); 125 | mockAppSpyObj.setPage.and.callThrough(); 126 | mockAppSpyObj.validatePage.calls.reset(); 127 | mockAppSpyObj.validatePage.and.callThrough(); 128 | mockAppSpyObj.validateVisual.calls.reset(); 129 | mockAppSpyObj.validateVisual.and.callThrough(); 130 | mockAppSpyObj.getVisualByName.calls.reset(); 131 | mockAppSpyObj.getVisualByName.and.callThrough(); 132 | mockAppSpyObj.getFilters.calls.reset(); 133 | mockAppSpyObj.getFilters.and.callThrough(); 134 | mockAppSpyObj.updateFilters.calls.reset(); 135 | mockAppSpyObj.updateFilters.and.callThrough(); 136 | mockAppSpyObj.setFilters.calls.reset(); 137 | mockAppSpyObj.setFilters.and.callThrough(); 138 | mockAppSpyObj.validateFilter.calls.reset(); 139 | mockAppSpyObj.validateFilter.and.callThrough(); 140 | mockAppSpyObj.addContextMenuCommand.calls.reset(); 141 | mockAppSpyObj.addContextMenuCommand.and.callThrough(); 142 | mockAppSpyObj.addOptionsMenuCommand.calls.reset(); 143 | mockAppSpyObj.addOptionsMenuCommand.and.callThrough(); 144 | mockAppSpyObj.removeContextMenuCommand.calls.reset(); 145 | mockAppSpyObj.removeContextMenuCommand.and.callThrough(); 146 | mockAppSpyObj.removeOptionsMenuCommand.calls.reset(); 147 | mockAppSpyObj.removeOptionsMenuCommand.and.callThrough(); 148 | mockAppSpyObj.print.calls.reset(); 149 | mockAppSpyObj.print.and.callThrough(); 150 | mockAppSpyObj.refreshData.calls.reset(); 151 | mockAppSpyObj.refreshData.and.callThrough(); 152 | mockAppSpyObj.exportData.calls.reset(); 153 | mockAppSpyObj.exportData.and.callThrough(); 154 | mockAppSpyObj.validateCreateReport.calls.reset(); 155 | mockAppSpyObj.validateCreateReport.and.callThrough(); 156 | mockAppSpyObj.validateQuickCreate.calls.reset(); 157 | mockAppSpyObj.validateQuickCreate.and.callThrough(); 158 | mockAppSpyObj.switchMode.calls.reset(); 159 | mockAppSpyObj.switchMode.and.callThrough(); 160 | mockAppSpyObj.save.calls.reset(); 161 | mockAppSpyObj.save.and.callThrough(); 162 | mockAppSpyObj.saveAs.calls.reset(); 163 | mockAppSpyObj.saveAs.and.callThrough(); 164 | mockAppSpyObj.setAccessToken.calls.reset(); 165 | mockAppSpyObj.setAccessToken.and.callThrough(); 166 | mockAppSpyObj.switchLayout.calls.reset(); 167 | mockAppSpyObj.switchLayout.and.callThrough(); 168 | } 169 | }; 170 | 171 | export const mockApp: IApp = mockAppSpyObj; 172 | -------------------------------------------------------------------------------- /test/utility/mockEmbed.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | import { WindowPostMessageProxy } from 'window-post-message-proxy'; 5 | import { HttpPostMessage } from 'http-post-message'; 6 | import { Router } from 'powerbi-router'; 7 | import { mockAppSpyObj, mockApp } from './mockApp'; 8 | import * as models from 'powerbi-models'; 9 | 10 | export const spyApp = mockAppSpyObj; 11 | 12 | export function setupEmbedMockApp(iframeContentWindow: Window, parentWindow: Window, name: string = 'MockAppWindowPostMessageProxy'): HttpPostMessage { 13 | const parent = parentWindow || iframeContentWindow.parent; 14 | const wpmp = new WindowPostMessageProxy({ 15 | processTrackingProperties: { 16 | addTrackingProperties: HttpPostMessage.addTrackingProperties, 17 | getTrackingProperties: HttpPostMessage.getTrackingProperties, 18 | }, 19 | isErrorMessage: HttpPostMessage.isErrorMessage, 20 | receiveWindow: iframeContentWindow, 21 | name, 22 | }); 23 | const hpm = new HttpPostMessage(wpmp, { 24 | 'origin': 'reportEmbedMock', 25 | 'x-version': '1.0.0' 26 | }, parent); 27 | const router = new Router(wpmp); 28 | const app = mockApp; 29 | 30 | /** 31 | * Setup not found handlers. 32 | */ 33 | function notFoundHandler(req, res): void { 34 | res.send(404, `Not Found. Url: ${req.params.notfound} was not found.`); 35 | } 36 | router.get('*notfound', notFoundHandler); 37 | router.post('*notfound', notFoundHandler); 38 | router.patch('*notfound', notFoundHandler); 39 | router.put('*notfound', notFoundHandler); 40 | router.delete('*notfound', notFoundHandler); 41 | 42 | /** 43 | * Dashboard Embed 44 | */ 45 | router.post('/dashboard/load', async (req, res) => { 46 | const uniqueId = req.headers['uid']; 47 | const loadConfig = req.body; 48 | try { 49 | await app.validateDashboardLoad(loadConfig); 50 | try { 51 | await app.dashboardLoad(loadConfig); 52 | hpm.post(`/dashboards/${uniqueId}/events/loaded`, { 53 | initiator: "sdk" 54 | }); 55 | } catch (error) { 56 | hpm.post(`/dashboards/${uniqueId}/events/error`, error); 57 | } 58 | res.send(202, {}); 59 | } catch (error) { 60 | res.send(400, error); 61 | } 62 | }); 63 | 64 | /** 65 | * Create Report 66 | */ 67 | router.post('/report/create', async (req, res) => { 68 | const uniqueId = req.headers['uid']; 69 | const createConfig = req.body; 70 | try { 71 | await app.validateCreateReport(createConfig); 72 | try { 73 | await app.reportLoad(createConfig); 74 | hpm.post(`/reports/${uniqueId}/events/loaded`, { 75 | initiator: "sdk" 76 | }); 77 | } catch (error) { 78 | hpm.post(`/reports/${uniqueId}/events/error`, error); 79 | } 80 | res.send(202, {}); 81 | } catch (error) { 82 | res.send(400, error); 83 | } 84 | }); 85 | 86 | /** 87 | * Quick Create 88 | */ 89 | router.post('/quickcreate', (req, res) => { 90 | const createConfig = req.body; 91 | return app.validateQuickCreate(createConfig) 92 | .then(() => { 93 | res.send(202); 94 | }, error => { 95 | res.send(400, error); 96 | }); 97 | }); 98 | 99 | /** 100 | * Report Embed 101 | */ 102 | router.post('/report/load', async (req, res) => { 103 | const uniqueId = req.headers['uid']; 104 | const loadConfig = req.body; 105 | try { 106 | await app.validateReportLoad(loadConfig); 107 | try { 108 | await app.reportLoad(loadConfig); 109 | hpm.post(`/reports/${uniqueId}/events/loaded`, { 110 | initiator: "sdk" 111 | }); 112 | } catch (error) { 113 | hpm.post(`/reports/${uniqueId}/events/error`, error); 114 | } 115 | res.send(202, {}); 116 | 117 | } catch (error) { 118 | res.send(400, error); 119 | } 120 | }); 121 | 122 | /** 123 | * Report Embed 124 | */ 125 | router.post('/report/prepare', async (req, res) => { 126 | const uniqueId = req.headers['uid']; 127 | const loadConfig = req.body; 128 | try { 129 | await app.validateReportLoad(loadConfig); 130 | try { 131 | await app.reportLoad(loadConfig); 132 | hpm.post(`/reports/${uniqueId}/events/loaded`, { 133 | initiator: "sdk" 134 | }); 135 | } catch (error) { 136 | hpm.post(`/reports/${uniqueId}/events/error`, error); 137 | } 138 | res.send(202, {}); 139 | 140 | } catch (error) { 141 | res.send(400, error); 142 | } 143 | }); 144 | 145 | router.post('/report/render', (req, res) => { 146 | app.render(); 147 | res.send(202, {}); 148 | }); 149 | 150 | router.get('/report/pages', async (req, res) => { 151 | try { 152 | const pages = await app.getPages(); 153 | res.send(200, pages); 154 | 155 | } catch (error) { 156 | res.send(500, error); 157 | } 158 | }); 159 | 160 | router.put('/report/pages/active', async (req, res) => { 161 | const uniqueId = req.headers['uid']; 162 | const page = req.body; 163 | try { 164 | await app.validatePage(page); 165 | try { 166 | await app.setPage(page); 167 | hpm.post(`/reports/${uniqueId}/events/pageChanged`, { 168 | initiator: "sdk", 169 | newPage: page 170 | }); 171 | } catch (error) { 172 | hpm.post(`/reports/${uniqueId}/events/error`, error); 173 | } 174 | res.send(202); 175 | } catch (error) { 176 | res.send(400, error); 177 | } 178 | }); 179 | 180 | router.get('/report/filters', async (req, res) => { 181 | try { 182 | const filters = await app.getFilters(); 183 | res.send(200, filters); 184 | } catch (error) { 185 | res.send(500, error); 186 | } 187 | }); 188 | 189 | router.put('/report/filters', async (req, res) => { 190 | const uniqueId = req.headers['uid']; 191 | const filters = req.body; 192 | 193 | try { 194 | await Promise.all(filters.map(filter => app.validateFilter(filter))); 195 | try { 196 | const filter = await app.setFilters(filters); 197 | hpm.post(`/reports/${uniqueId}/events/filtersApplied`, { 198 | initiator: "sdk", 199 | filter 200 | }); 201 | } catch (error) { 202 | hpm.post(`/reports/${uniqueId}/events/error`, error); 203 | } 204 | res.send(202, {}); 205 | } catch (error) { 206 | res.send(400, error); 207 | } 208 | }); 209 | 210 | router.post('/report/filters', async (req, res) => { 211 | const uniqueId = req.headers['uid']; 212 | const operation = req.body.filtersOperation; 213 | const filters = req.body.filters; 214 | 215 | try { 216 | Promise.all(filters ? filters.map(filter => app.validateFilter(filter)) : [Promise.resolve(null)]); 217 | try { 218 | const filter = await app.updateFilters(operation, filters); 219 | hpm.post(`/reports/${uniqueId}/events/filtersApplied`, { 220 | initiator: "sdk", 221 | filter 222 | }); 223 | } catch (error) { 224 | hpm.post(`/reports/${uniqueId}/events/error`, error); 225 | } 226 | res.send(202, {}); 227 | } catch (error) { 228 | res.send(400, error); 229 | } 230 | }); 231 | 232 | router.get('/report/pages/:pageName/filters', async (req, res) => { 233 | const page = { 234 | name: req.params.pageName, 235 | displayName: null 236 | }; 237 | try { 238 | await app.validatePage(page); 239 | try { 240 | const filters = await app.getFilters(); 241 | res.send(200, filters); 242 | } catch (error) { 243 | res.send(500, error); 244 | } 245 | } catch (error) { 246 | res.send(400, error); 247 | } 248 | }); 249 | 250 | router.post('/report/pages/:pageName/filters', async (req, res) => { 251 | const pageName = req.params.pageName; 252 | const uniqueId = req.headers['uid']; 253 | const operation = req.body.filtersOperation; 254 | const filters = req.body.filters; 255 | const page: models.IPage = { 256 | name: pageName, 257 | displayName: null 258 | }; 259 | 260 | try { 261 | await app.validatePage(page); 262 | await Promise.all(filters ? filters.map(filter => app.validateFilter(filter)) : [Promise.resolve(null)]); 263 | try { 264 | const filter = await app.updateFilters(operation, filters); 265 | hpm.post(`/reports/${uniqueId}/pages/${pageName}/events/filtersApplied`, { 266 | initiator: "sdk", 267 | filter 268 | }); 269 | } catch (error) { 270 | hpm.post(`/reports/${uniqueId}/events/error`, error); 271 | } 272 | res.send(202, {}); 273 | 274 | } catch (error) { 275 | res.send(400, error); 276 | 277 | } 278 | }); 279 | 280 | router.put('/report/pages/:pageName/filters', async (req, res) => { 281 | const pageName = req.params.pageName; 282 | const uniqueId = req.headers['uid']; 283 | const filters = req.body; 284 | const page: models.IPage = { 285 | name: pageName, 286 | displayName: null 287 | }; 288 | try { 289 | await app.validatePage(page); 290 | await Promise.all(filters.map(filter => app.validateFilter(filter))); 291 | try { 292 | const filter = await app.setFilters(filters); 293 | hpm.post(`/reports/${uniqueId}/pages/${pageName}/events/filtersApplied`, { 294 | initiator: "sdk", 295 | filter 296 | }); 297 | } catch (error) { 298 | hpm.post(`/reports/${uniqueId}/events/error`, error); 299 | } 300 | res.send(202, {}); 301 | } catch (error) { 302 | res.send(400, error); 303 | } 304 | }); 305 | 306 | router.get('/report/pages/:pageName/visuals/:visualName/filters', async (req, res) => { 307 | const page = { 308 | name: req.params.pageName, 309 | displayName: null 310 | }; 311 | const visual: models.IVisual = { 312 | name: req.params.visualName, 313 | title: 'title', 314 | type: 'type', 315 | layout: {}, 316 | }; 317 | 318 | try { 319 | await app.validateVisual(page, visual); 320 | try { 321 | const filters = await app.getFilters(); 322 | res.send(200, filters); 323 | } catch (error) { 324 | res.send(500, error); 325 | } 326 | } catch (error) { 327 | res.send(400, error); 328 | } 329 | }); 330 | 331 | router.post('/report/pages/:pageName/visuals/:visualName/filters', async (req, res) => { 332 | const pageName = req.params.pageName; 333 | const visualName = req.params.visualName; 334 | const uniqueId = req.headers['uid']; 335 | const operation = req.body.filtersOperation; 336 | const filters = req.body.filters; const page: models.IPage = { 337 | name: pageName, 338 | displayName: null 339 | }; 340 | const visual: models.IVisual = { 341 | name: visualName, 342 | title: 'title', 343 | type: 'type', 344 | layout: {}, 345 | }; 346 | 347 | try { 348 | await app.validateVisual(page, visual); 349 | await Promise.all(filters ? filters.map(filter => app.validateFilter(filter)) : [Promise.resolve(null)]); 350 | try { 351 | const filter = await app.updateFilters(operation, filters); 352 | hpm.post(`/reports/${uniqueId}/pages/${pageName}/visuals/${visualName}/events/filtersApplied`, { 353 | initiator: "sdk", 354 | filter 355 | }); 356 | } catch (error) { 357 | hpm.post(`/reports/${uniqueId}/events/error`, error); 358 | } 359 | res.send(202, {}); 360 | } catch (error) { 361 | res.send(400, error); 362 | } 363 | }); 364 | 365 | router.put('/report/pages/:pageName/visuals/:visualName/filters', async (req, res) => { 366 | const pageName = req.params.pageName; 367 | const visualName = req.params.visualName; 368 | const uniqueId = req.headers['uid']; 369 | const filters = req.body; 370 | const page: models.IPage = { 371 | name: pageName, 372 | displayName: null 373 | }; 374 | const visual: models.IVisual = { 375 | name: visualName, 376 | title: 'title', 377 | type: 'type', 378 | layout: {}, 379 | }; 380 | try { 381 | await app.validateVisual(page, visual); 382 | await Promise.all(filters.map(filter => app.validateFilter(filter))); 383 | try { 384 | const filter = await app.setFilters(filters); 385 | hpm.post(`/reports/${uniqueId}/pages/${pageName}/visuals/${visualName}/events/filtersApplied`, { 386 | initiator: "sdk", 387 | filter 388 | }); 389 | } catch (error) { 390 | hpm.post(`/reports/${uniqueId}/events/error`, error); 391 | } 392 | res.send(202, {}); 393 | } catch (error) { 394 | res.send(400, error); 395 | } 396 | }); 397 | 398 | router.patch('/report/settings', async (req, res) => { 399 | const uniqueId = req.headers['uid']; 400 | const settings = req.body; 401 | try { 402 | await app.validateSettings(settings); 403 | try { 404 | const updatedSettings = await app.updateSettings(settings); 405 | hpm.post(`/reports/${uniqueId}/events/settingsUpdated`, { 406 | initiator: "sdk", 407 | settings: updatedSettings 408 | }); 409 | } catch (error) { 410 | hpm.post(`/reports/${uniqueId}/events/error`, error); 411 | } 412 | res.send(202, {}); 413 | } 414 | catch (error) { 415 | res.send(400, error); 416 | } 417 | }); 418 | 419 | router.get('/report/data', async (req, res) => { 420 | const data = await app.exportData(); 421 | res.send(200, data); 422 | }); 423 | 424 | router.post('/report/refresh', (req, res) => { 425 | app.refreshData(); 426 | res.send(202); 427 | }); 428 | 429 | router.post('/report/print', (req, res) => { 430 | app.print(); 431 | res.send(202); 432 | }); 433 | 434 | router.post('report/switchMode/Edit', (req, res) => { 435 | app.switchMode(); 436 | res.send(202); 437 | }); 438 | 439 | router.post('report/save', (req, res) => { 440 | app.save(); 441 | res.send(202); 442 | }); 443 | 444 | router.post('report/saveAs', (req, res) => { 445 | const settings = req.body; 446 | app.saveAs(settings); 447 | res.send(202); 448 | }); 449 | 450 | router.post('report/token', (req, res) => { 451 | const settings = req.body; 452 | app.setAccessToken(settings); 453 | res.send(202); 454 | }); 455 | return hpm; 456 | } 457 | -------------------------------------------------------------------------------- /test/utility/mockHpm.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | export const spyHpm = { 5 | get: jasmine.createSpy("get").and.returnValue(Promise.resolve({})), 6 | post: jasmine.createSpy("post").and.returnValue(Promise.resolve({})), 7 | patch: jasmine.createSpy("patch").and.returnValue(Promise.resolve({})), 8 | put: jasmine.createSpy("put").and.returnValue(Promise.resolve({})), 9 | delete: jasmine.createSpy("delete").and.returnValue(Promise.resolve({})) 10 | }; 11 | -------------------------------------------------------------------------------- /test/utility/mockRouter.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | export const spyRouter = { 5 | get: jasmine.createSpy("get"), 6 | post: jasmine.createSpy("post"), 7 | patch: jasmine.createSpy("patch"), 8 | put: jasmine.createSpy("put"), 9 | delete: jasmine.createSpy("delete") 10 | }; 11 | -------------------------------------------------------------------------------- /test/utility/mockWpmp.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | export const spyWpmp = { 5 | handlers: [], 6 | 7 | clearHandlers(): void { 8 | spyWpmp.handlers.length = 0; 9 | }, 10 | 11 | addHandlerSpy(handler: any): void { 12 | spyWpmp.handlers.push(handler); 13 | }, 14 | 15 | addHandler: jasmine.createSpy("addHandler").and.callFake((x) => spyWpmp.addHandlerSpy(x)), 16 | 17 | postMessageSpy: jasmine.createSpy("postMessage"), 18 | postMessage(message: any): Promise { 19 | spyWpmp.postMessageSpy(message); 20 | return Promise.resolve(null); 21 | }, 22 | 23 | start: jasmine.createSpy("start"), 24 | stop: jasmine.createSpy("stop"), 25 | 26 | onMessageReceived(event: any): void { 27 | let message: any = event.data; 28 | 29 | const handled = spyWpmp.handlers.some(handler => { 30 | if (handler.test(message)) { 31 | Promise.resolve(handler.handle(message)); 32 | 33 | return true; 34 | } 35 | }); 36 | 37 | if (!handled) { 38 | throw Error(`nothing handled message`); 39 | } 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /test/utility/noop.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Iframe No-Op tester 6 | 7 | 8 |

Dummy Iframe

9 |

Does nothing.

10 | 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "noImplicitAny": false, 5 | "sourceMap": true, 6 | "noImplicitUseStrict": true, 7 | "outDir": "dist" 8 | }, 9 | "exclude": [ 10 | "node_modules", 11 | "dist", 12 | "docs", 13 | "test", 14 | "tmp", 15 | ".publish" 16 | ] 17 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var package = require('./package.json'); 2 | 3 | module.exports = { 4 | entry: { 5 | 'powerbi': './src/powerbi-client.ts' 6 | }, 7 | output: { 8 | globalObject: "this", 9 | path: __dirname + "/dist", 10 | filename: '[name].js', 11 | library: package.name, 12 | libraryTarget: 'umd' 13 | }, 14 | devtool: 'source-map', 15 | resolve: { 16 | extensions: ['.webpack.js', '.web.js', '.ts', '.js'] 17 | }, 18 | module: { 19 | rules: [ 20 | { test: /\.map$/, loader: 'ignore-loader' }, 21 | { test: /\.d.ts$/, loader: 'ignore-loader' }, 22 | { test: /\.ts$/, exclude: /\.d.ts$/, loader: 'ts-loader' }, 23 | { test: /\.json$/, loader: 'json-loader' } 24 | ] 25 | } 26 | } -------------------------------------------------------------------------------- /webpack.test.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); // To access built-in plugins 2 | const glob = require("glob"); 3 | 4 | module.exports = { 5 | mode: 'development', 6 | entry: glob.sync('./test/**/*.spec.ts'), 7 | output: { 8 | path: __dirname + "/tmp", 9 | filename: 'test.spec.js' 10 | }, 11 | devtool: 'source-map', 12 | resolve: { 13 | extensions: ['.webpack.js', '.web.js', '.ts', '.js'] 14 | }, 15 | module: { 16 | rules: [ 17 | { test: /\.map$/, loader: 'ignore-loader' }, 18 | { test: /\.d.ts$/, loader: 'ignore-loader' }, 19 | { test: /\.ts$/, exclude: /\.d.ts$/, loader: 'ts-loader' }, 20 | { test: /\.json$/, loader: 'json-loader' } 21 | ] 22 | }, 23 | plugins: [ 24 | new webpack.LoaderOptionsPlugin({ 25 | ts: { 26 | configFileName: "webpack.test.tsconfig.json" 27 | } 28 | }) 29 | ], 30 | } -------------------------------------------------------------------------------- /webpack.test.tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "noImplicitAny": false, 5 | "sourceMap": true 6 | }, 7 | "exclude": [ 8 | "node_modules", 9 | "dist", 10 | "docs", 11 | "tmp", 12 | ".publish" 13 | ] 14 | } --------------------------------------------------------------------------------