Merged PR 83686: React Wrapper
React Wrapper library for PowerBI-JavaScript ###Limitations as discussed: |Sr. no.| Limitation| |:------|:------| |1|`powerbi.preload()` is not supported| |2|`powerbi.load()` is not supported as of now| |3|Power BI Report Authoring support is removed as of now, until the import issue gets fixed| ## Licenses (Dependency) |Package| License | |:------|:------:| |powerbi-client|MIT| |powerbi-report-authoring|MIT| ## Licenses (Dev dependency) |Package| License | |:------|:------:| |@types/react<br/> react|MIT| |@types/react-dom<br/> react-dom|MIT| |@types/jasmine<br/>jasmine-core|MIT| |eslint|MIT| |eslint-plugin-react|MIT| |karma|MIT| |karma-chrome-launcher|MIT| |karma-jasmine|MIT| |ts-loader|MIT| |typescript|Apache-2.0| |webpack|MIT| |webpack-cli|MIT| |@typescript-eslint/eslint-plugin|MIT| |@typescript-eslint/parser|MIT| ## Licenses (Demo) |Package| License | |:------|:------:| |webpack-dev-server|MIT| |style-loader|MIT| |css-loader|MIT| Related work items: #299176, #354581, #355208, #355675, #356714, #357149, #357176, #357181, #357906, #362139, #366497, #367147, #368022
This commit is contained in:
Родитель
da95abd107
Коммит
57dd3e651a
|
@ -0,0 +1,34 @@
|
|||
module.exports = {
|
||||
parser: "@typescript-eslint/parser", // Specifies the ESLint parser
|
||||
extends: [
|
||||
"plugin:react/recommended", // Uses the recommended rules from @eslint-plugin-react
|
||||
"plugin:@typescript-eslint/recommended" // Uses the recommended rules from @typescript-eslint/eslint-plugin
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true // Allows for the parsing of JSX
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/no-this-alias': [
|
||||
'error',
|
||||
{
|
||||
allowDestructuring: true, // Allow `const { props, state } = this`; false by default
|
||||
allowedNames: ['thisObj'], // Allow `const self = this`; `[]` by default
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-empty-interface': [
|
||||
'error',
|
||||
{
|
||||
allowSingleExtends: true
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-extra-semi": "off"
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
version: "detect" // Tells eslint-plugin-react to automatically detect the version of React to use
|
||||
}
|
||||
}
|
||||
};
|
|
@ -0,0 +1,27 @@
|
|||
# dependencies
|
||||
**/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
**/package-lock.json
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
/compiledTests
|
||||
|
||||
# production
|
||||
/dist
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
.vscode
|
||||
|
||||
*.tgz
|
|
@ -0,0 +1,20 @@
|
|||
$exitCode = 0;
|
||||
|
||||
Write-Host "start: npm run build"
|
||||
& npm run build
|
||||
Write-Host "done: npm run build"
|
||||
|
||||
$exitCode += $LASTEXITCODE;
|
||||
|
||||
# Check linting
|
||||
Write-Host "start: npm run lint"
|
||||
& npm run lint
|
||||
Write-Host "done: npm run lint"
|
||||
|
||||
$exitCode += $LASTEXITCODE;
|
||||
|
||||
Write-Host "start: Get dist folder files"
|
||||
& dir "dist"
|
||||
Write-Host "Done: Get dist folder files"
|
||||
|
||||
exit $exitCode
|
|
@ -0,0 +1,6 @@
|
|||
setlocal enabledelayedexpansion
|
||||
pushd "%~dp0\.."
|
||||
powershell.exe -ExecutionPolicy Unrestricted -NoProfile -WindowStyle Hidden -NonInteractive -File "%~dp0%~1"
|
||||
endlocal
|
||||
popd
|
||||
exit /B %ERRORLEVEL%
|
|
@ -0,0 +1,13 @@
|
|||
$exitCode = 0;
|
||||
|
||||
Write-Host "start: npm pack"
|
||||
& npm pack
|
||||
Write-Host "done: npm pack"
|
||||
|
||||
$exitCode += $LASTEXITCODE;
|
||||
|
||||
Write-Host "start: Get content of current folder"
|
||||
& dir
|
||||
Write-Host "done: Get content of current folder"
|
||||
|
||||
exit $exitCode
|
|
@ -0,0 +1,83 @@
|
|||
environment:
|
||||
host:
|
||||
os: 'windows'
|
||||
flavor: 'server'
|
||||
version: '2016'
|
||||
runtime:
|
||||
provider: 'appcontainer'
|
||||
image: 'cdpxwinrs5.azurecr.io/global/vse2019/16.3.7:latest'
|
||||
source_mode: 'map'
|
||||
|
||||
artifact_publish_options:
|
||||
publish_to_legacy_artifacts: false
|
||||
publish_to_pipeline_artifacts: true
|
||||
publish_to_cloudvault_artifacts: false
|
||||
|
||||
package_sources:
|
||||
npm:
|
||||
feeds:
|
||||
registry: https://powerbi.pkgs.visualstudio.com/_packaging/SDK.Externals/npm/registry/
|
||||
|
||||
version:
|
||||
major: 1 # <---- Required field but not being used for 'custom' scheme
|
||||
minor: 0 # <---- Required field but not being used for 'custom' scheme
|
||||
system: 'custom' # <---- Set this to 'custom'. we will build the version using package.json in versioning commands.
|
||||
|
||||
versioning:
|
||||
commands:
|
||||
- !!defaultcommand
|
||||
name: 'Set Version'
|
||||
arguments: 'version.ps1'
|
||||
command: '.pipelines\cdpx_run_ps.cmd'
|
||||
|
||||
restore:
|
||||
commands:
|
||||
- !!defaultcommand
|
||||
name: 'NPM Install'
|
||||
arguments: 'restore.ps1'
|
||||
command: '.pipelines\cdpx_run_ps.cmd'
|
||||
|
||||
build:
|
||||
commands:
|
||||
- !!buildcommand
|
||||
name: 'Build'
|
||||
arguments: 'build.ps1'
|
||||
command: '.pipelines\cdpx_run_ps.cmd'
|
||||
artifacts:
|
||||
- from: 'dist'
|
||||
to: 'build_artifacts'
|
||||
include:
|
||||
- '**/*'
|
||||
exclude:
|
||||
- '**/node_modules/**/*.*'
|
||||
- to: 'source'
|
||||
include:
|
||||
- '**/*'
|
||||
exclude:
|
||||
- '**/.pipelines/**/*.*'
|
||||
- '**/.vscode/**/*.*'
|
||||
- '**/test/**/*.*'
|
||||
- '**/demo/**/*.*'
|
||||
- '**/dist/**/*.*'
|
||||
- '**/node_modules/**/*.*'
|
||||
|
||||
- !!buildcommand
|
||||
name: 'Package'
|
||||
arguments: 'package.ps1'
|
||||
command: '.pipelines\cdpx_run_ps.cmd'
|
||||
artifacts:
|
||||
- include:
|
||||
- "**/*.tgz"
|
||||
|
||||
test:
|
||||
commands:
|
||||
- !!testcommand
|
||||
name: 'Test powerbi-client-react'
|
||||
arguments: 'test.ps1'
|
||||
command: '.pipelines\cdpx_run_ps.cmd'
|
||||
testresults:
|
||||
- title: 'powerbi-client-react test results'
|
||||
type: 'jasmine'
|
||||
from: 'reports'
|
||||
# include:
|
||||
# - "**coverage/**/index.html"
|
|
@ -0,0 +1,25 @@
|
|||
Write-Host "Start build ..."
|
||||
Write-Host "Global node/npm paths ..."
|
||||
& where.exe npm
|
||||
& where.exe node
|
||||
|
||||
Write-Host "Global node version"
|
||||
& node -v
|
||||
|
||||
Write-Host "Global npm version"
|
||||
& npm -v
|
||||
|
||||
$exitCode = 0;
|
||||
|
||||
Write-Host "start: try install latest npm version"
|
||||
& npm install npm@latest -g
|
||||
Write-Host "done: try install latest npm version"
|
||||
|
||||
# Do not update $exitCode because we do not want to fail if install latest npm version fails.
|
||||
|
||||
Write-Host "start: npm install"
|
||||
& npm install --no-audit --no-save
|
||||
Write-Host "done: npm install"
|
||||
$exitCode += $LASTEXITCODE;
|
||||
|
||||
exit $exitCode
|
|
@ -0,0 +1,9 @@
|
|||
$exitCode = 0;
|
||||
|
||||
Write-Host "start: npm run test"
|
||||
& npm run test
|
||||
Write-Host "done: npm run test"
|
||||
|
||||
$exitCode += $LASTEXITCODE;
|
||||
|
||||
exit $exitCode;
|
|
@ -0,0 +1,15 @@
|
|||
try {
|
||||
# package.json is in root folder, while version.ps1 runs in .pipelines folder.
|
||||
$version = (Get-Content "package.json") -join "`n" | ConvertFrom-Json | Select -ExpandProperty "version"
|
||||
$revision = $env:CDP_DEFINITION_BUILD_COUNT_DAY
|
||||
$buildNumber = "$version.$revision"
|
||||
|
||||
Write-Host "Build Number is" $buildNumber
|
||||
|
||||
[Environment]::SetEnvironmentVariable("CustomBuildNumber", $buildNumber, "User") # This will allow you to use it from env var in later steps of the same phase
|
||||
Write-Host "##vso[build.updatebuildnumber]${buildNumber}" # This will update build number on your build
|
||||
}
|
||||
catch {
|
||||
Write-Error $_.Exception
|
||||
exit 1;
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
# Contributing
|
||||
|
||||
## Setup
|
||||
|
||||
Clone the repository:
|
||||
```
|
||||
git clone <url>
|
||||
```
|
||||
|
||||
Navigate to the cloned directory
|
||||
|
||||
Install local dependencies:
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
## Build:
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
Or if using VScode: `Ctrl + Shift + B`
|
||||
|
||||
## Test
|
||||
```
|
||||
npm test
|
||||
```
|
||||
By default the tests run using Chrome
|
||||
|
||||
The build and tests use webpack to compile all the source modules into one bundled module that can be executed in the browser.
|
||||
|
||||
## Running the demo
|
||||
|
||||
If you want to embed any powerbi artifact in demo, set the `embedUrl` and `accessToken` in the [config file](demo\config.ts) for that artifact type.
|
||||
|
||||
Serve the demo:
|
||||
```
|
||||
npm run demo
|
||||
```
|
||||
|
||||
Open the address to view in the browser:
|
||||
|
||||
http://localhost:8080/
|
|
@ -0,0 +1,13 @@
|
|||
powerbi-client-react
|
||||
|
||||
Copyright (c) Microsoft Corporation
|
||||
|
||||
All rights reserved.
|
||||
|
||||
MIT License
|
||||
|
||||
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:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
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.
|
146
README.md
146
README.md
|
@ -1,20 +1,134 @@
|
|||
# Introduction
|
||||
TODO: Give a short introduction of your project. Let this section explain the objectives or the motivation behind this project.
|
||||
# powerbi-client-react
|
||||
A React wrapper library for embedding PowerBI artifacts.
|
||||
|
||||
# Getting Started
|
||||
TODO: Guide users through getting your code up and running on their own system. In this section you can talk about:
|
||||
1. Installation process
|
||||
2. Software dependencies
|
||||
3. Latest releases
|
||||
4. API references
|
||||
## Table of contents
|
||||
|
||||
# Build and Test
|
||||
TODO: Describe and show how to build your code and run the tests.
|
||||
<!--ts-->
|
||||
* [Sample Usage](#sample-usage)
|
||||
* [Run Demo](#run-demo)
|
||||
* [Docs](#docs)
|
||||
* Props interface
|
||||
* PowerBI Embed
|
||||
* Get reference to embedded object
|
||||
* How to set new accessToken
|
||||
* Set event handlers
|
||||
* Reset event handlers
|
||||
* Apply style class
|
||||
* Update settings (Report only)
|
||||
* PowerBI Bootstrap
|
||||
* [Flow diagram](#flow-diagram-for-the-wrapper-component)
|
||||
* [Dependencies](#dependencies)
|
||||
<!--te-->
|
||||
|
||||
# Contribute
|
||||
TODO: Explain how other users and developers can contribute to make your code better.
|
||||
## Sample Usage
|
||||
|
||||
If you want to learn more about creating good readme files then refer the following [guidelines](https://docs.microsoft.com/en-us/azure/devops/repos/git/create-a-readme?view=azure-devops). You can also seek inspiration from the below readme files:
|
||||
- [ASP.NET Core](https://github.com/aspnet/Home)
|
||||
- [Visual Studio Code](https://github.com/Microsoft/vscode)
|
||||
- [Chakra Core](https://github.com/Microsoft/ChakraCore)
|
||||
How to import:
|
||||
|
||||
```jsx
|
||||
import { PowerBIEmbed } from 'powerbi-client-react';
|
||||
```
|
||||
|
||||
How to bootstrap a PowerBI report:
|
||||
```jsx
|
||||
<PowerBIEmbed
|
||||
embedConfig = {{
|
||||
type: 'report', // Supported types: report, dashboard, tile, visual and qna
|
||||
id: undefined,
|
||||
embedUrl: undefined,
|
||||
accessToken: undefined, // Keep as empty string, null or undefined
|
||||
tokenType: models.TokenType.Embed
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
How to embed a PowerBI report:
|
||||
```jsx
|
||||
<PowerBIEmbed
|
||||
embedConfig = {{
|
||||
type: 'report', // Supported types: report, dashboard, tile, visual and qna
|
||||
id: '<Report Id>',
|
||||
embedUrl: '<Embed Url>',
|
||||
accessToken: '<Access Token>',
|
||||
tokenType: models.TokenType.Embed,
|
||||
settings: {
|
||||
panes: {
|
||||
filters: {
|
||||
expanded: false,
|
||||
visible: false
|
||||
}
|
||||
},
|
||||
background: models.BackgroundType.Transparent,
|
||||
}
|
||||
}}
|
||||
|
||||
eventHandlers = {
|
||||
new Map([
|
||||
['loaded', function () {console.log('Report loaded');}],
|
||||
['rendered', function () {console.log('Report rendered');}],
|
||||
['error', function (event) {console.log(event.detail);}]
|
||||
])}
|
||||
|
||||
cssClassName = { "report-style-class" }
|
||||
|
||||
getEmbed = { (embeddedReport) => {
|
||||
this.report = embeddedReport as Report;
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
## Run Demo
|
||||
|
||||
To run the demo on localhost, run the following commands:
|
||||
|
||||
```
|
||||
npm install
|
||||
npm run install:demo
|
||||
npm run demo
|
||||
```
|
||||
|
||||
Redirect to http://localhost:8080/ to view in the browser.
|
||||
|
||||
## Docs
|
||||
|Topic|Details|
|
||||
|:------|:------|
|
||||
|PowerBI Embed|To embed your powerbi artifact, pass the component with atleast _type_, _embedUrl_ and _accessToken_ in _embedConfig_ prop.|
|
||||
|Get reference to embedded object|Pass a callback method which accepts the embedded object as parameter to the _getEmbed_ of props.<br/>Refer to the _getEmbed_ prop in [Sample Usage](#sample-usage).|
|
||||
|Apply style class|Pass the name(s) of classes to be set as "classname" for the embed container div via _className_ of props.|
|
||||
|Set event handlers|Pass a map object of event name (string) and event handler (function) to the _eventHandlers_ of props. <br/>Key: Event name <br/>Value: Method to be triggered|
|
||||
|Reset event handlers|To reset event handler for an event, set the event handler's value as `null` in the _eventHandlers_ map of props.|
|
||||
|How to set new accessToken|To set new accessToken in the same embedded powerbi artifact, pass the updated _accessToken_ in _embedConfig_ of props.<br/>Example scenario: _Current token has expired_.|
|
||||
|Update settings (Report type only)|To update the report settings, update the _embedConfig.settings_ property of props.<br/>Refer to the _embedConfig.settings_ prop in [Sample Usage](#sample-usage).|
|
||||
|PowerBI Bootstrap|To [bootstrap your powerbi entity](https://github.com/microsoft/PowerBI-JavaScript/wiki/Bootstrap-For-Better-Performance), call the component without _accessToken_ in _embedConfig_ of props.<br/>__Note__: _embedConfig_ of props should atleast contain __type__ of the powerbi artifact being embedded. <br/>Eg: "report", "dashboard", "tile", "visual" or "qna".<br/>Refer How to bootstrap a report section in [Sample Usage](#sample-usage).|
|
||||
|
||||
### Props interface:
|
||||
|
||||
```ts
|
||||
interface EmbedProps {
|
||||
|
||||
// Configuration for embedding the PowerBI entity
|
||||
embedConfig: IEmbedConfiguration | IQnaEmbedConfiguration
|
||||
|
||||
// Callback method to get the embedded PowerBI entity object (Optional)
|
||||
getEmbed?: { (embeddedComponent: Embed): void }
|
||||
|
||||
// Map of pair of event name and its handler method to be triggered on the event (Optional)
|
||||
eventHandlers?: Map<string, service.IEventHandler<any> | null>
|
||||
|
||||
// CSS class to be set on the embedding container (Optional)
|
||||
cssClassName?: string
|
||||
|
||||
// Provide a custom implementation of PowerBI service (Optional)
|
||||
service?: service.Service
|
||||
}
|
||||
```
|
||||
|
||||
### Flow Diagram for the Wrapper Component:
|
||||
![Flow Diagram](./resources/react_wrapper_flow_diagram.png)
|
||||
|
||||
## Dependencies
|
||||
|
||||
1. powerbi-client
|
||||
|
||||
## Peer-Dependencies
|
||||
|
||||
1. react
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"include": [
|
||||
"../../src/**/*.tsx",
|
||||
"../../src/**/*.ts"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"lib": ["ES2016"],
|
||||
"target": "es5",
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"noErrorTruncation": true,
|
||||
"module": "ES6",
|
||||
"moduleResolution": "node",
|
||||
"jsx": "react",
|
||||
"sourceMap": true,
|
||||
"noImplicitAny": true,
|
||||
"declaration": true,
|
||||
"outDir": "../../dist",
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
let path = require('path');
|
||||
|
||||
module.exports = {
|
||||
entry: path.resolve('src/PowerBIEmbed.tsx'),
|
||||
output: {
|
||||
library: 'powerbi-client-react',
|
||||
libraryTarget: 'amd',
|
||||
path: path.resolve('dist'),
|
||||
filename: 'powerbi-client-react.js'
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.ts(x)?$/,
|
||||
loader: 'ts-loader',
|
||||
options: {
|
||||
configFile: path.resolve('config/src/tsconfig.json')
|
||||
},
|
||||
exclude: /node_modules/
|
||||
},
|
||||
]
|
||||
},
|
||||
resolve: {
|
||||
modules: ['node_modules'],
|
||||
extensions: [
|
||||
'.tsx',
|
||||
'.ts',
|
||||
'.js'
|
||||
]
|
||||
},
|
||||
devtool: 'source-map',
|
||||
};
|
|
@ -0,0 +1,53 @@
|
|||
let path = require('path');
|
||||
|
||||
module.exports = function(config) {
|
||||
config.set({
|
||||
|
||||
// base path that will be used to resolve all patterns (eg. files, exclude)
|
||||
basePath: '',
|
||||
|
||||
// frameworks to use
|
||||
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
|
||||
frameworks: ['jasmine'],
|
||||
|
||||
// list of files / patterns to load in the browser
|
||||
files: [
|
||||
path.resolve('compiledTests/**/*spec.js')
|
||||
],
|
||||
|
||||
// preprocess matching files before serving them to the browser
|
||||
// available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
|
||||
preprocessors: {
|
||||
},
|
||||
|
||||
// test results reporter to use
|
||||
// possible values: 'dots', 'progress'
|
||||
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
|
||||
reporters: ['progress'],
|
||||
|
||||
// web server port
|
||||
port: 9876,
|
||||
|
||||
// enable / disable colors in the output (reporters and logs)
|
||||
colors: true,
|
||||
|
||||
// level of logging
|
||||
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
|
||||
logLevel: config.LOG_INFO,
|
||||
|
||||
// enable / disable watching file and executing tests whenever any file changes
|
||||
autoWatch: false,
|
||||
|
||||
// start these browsers
|
||||
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
|
||||
browsers: ['ChromeHeadless'],
|
||||
|
||||
// Continuous Integration mode
|
||||
// if true, Karma captures browsers, runs the tests and exits
|
||||
singleRun: true,
|
||||
|
||||
// Concurrency level
|
||||
// how many browser should be started simultaneous
|
||||
concurrency: Infinity
|
||||
})
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"include": [
|
||||
"../../test/**/*.tsx",
|
||||
"../../test/**/*.ts"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"lib": [
|
||||
"ES2016",
|
||||
"dom"
|
||||
],
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": false,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"jsx": "react"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
let path = require('path');
|
||||
|
||||
module.exports = {
|
||||
mode: 'development',
|
||||
entry: {
|
||||
PowerBIEmbedTest: path.resolve('test/PowerBIEmbed.spec.tsx'),
|
||||
utilsTest: path.resolve('test/utils.spec.ts'),
|
||||
},
|
||||
output: {
|
||||
path: path.resolve('compiledTests'),
|
||||
filename: '[name].spec.js'
|
||||
},
|
||||
devtool: 'source-map',
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.ts(x)?$/,
|
||||
loader: 'ts-loader',
|
||||
options: {
|
||||
configFile: path.resolve('config/test/tsconfig.json')
|
||||
},
|
||||
exclude: /node_modules/
|
||||
},
|
||||
]
|
||||
},
|
||||
resolve: {
|
||||
extensions: [
|
||||
'.tsx',
|
||||
'.ts',
|
||||
'.js'
|
||||
]
|
||||
},
|
||||
};
|
|
@ -0,0 +1,24 @@
|
|||
.report-style-class {
|
||||
height: 560px;
|
||||
width: 960px;
|
||||
}
|
||||
|
||||
body {
|
||||
padding: 24px;
|
||||
font-family: 'Segoe UI';
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: #175C97;
|
||||
}
|
||||
|
||||
button {
|
||||
background: #337AB7;
|
||||
border-radius: 5px;
|
||||
margin-right: 15px;
|
||||
color: #FFFFFF;
|
||||
height: 35px;
|
||||
width: 150px;
|
||||
font-size: medium;
|
||||
border: 0;
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
import React, { useState } from 'react';
|
||||
import { models, Report } from 'powerbi-client';
|
||||
import { PowerBIEmbed } from 'powerbi-client-react';
|
||||
import './DemoApp.css';
|
||||
|
||||
// Root Component to demonstrate usage of wrapper component
|
||||
function DemoApp () {
|
||||
|
||||
// PowerBI Report object (to be received via callback)
|
||||
let report: Report;
|
||||
|
||||
// API end-point url to get embed config for a sample report
|
||||
const sampleReportUrl = 'https://aka.ms/sampleReportEmbedConfig';
|
||||
|
||||
// Report config useState hook
|
||||
// Values for properties like embedUrl, accessToken and settings will be set on click of buttons below
|
||||
const [sampleReportConfig, setReportConfig] = useState({
|
||||
type: 'report',
|
||||
embedUrl: undefined,
|
||||
tokenType: models.TokenType.Embed,
|
||||
accessToken: undefined,
|
||||
settings: undefined,
|
||||
});
|
||||
|
||||
// Map of event handlers to be applied to the embedding report
|
||||
const eventHandlersMap = new Map([
|
||||
['loaded', function () {console.log('Report has loaded');}],
|
||||
['rendered', function () {
|
||||
console.log('Report has rendered');
|
||||
// Update display message
|
||||
setMessage('Report is Embedded!')
|
||||
}],
|
||||
['error', function (event) { console.error(event.detail); }]
|
||||
]);
|
||||
|
||||
// Fetch sample report's config (eg. embedUrl and AccessToken) for embedding
|
||||
const mockSignIn = async () => {
|
||||
|
||||
// Update display message
|
||||
setMessage('Fetching accessToken')
|
||||
|
||||
const reportConfigResponse = await fetch(sampleReportUrl);
|
||||
|
||||
if (!reportConfigResponse.ok) {
|
||||
console.error(`Failed to fetch config for report. Status: ${ reportConfigResponse.status } ${ reportConfigResponse.statusText }`);
|
||||
return;
|
||||
}
|
||||
|
||||
const reportConfig = await reportConfigResponse.json();
|
||||
|
||||
// Update display message
|
||||
setMessage('AccessToken is set successfully. Loading the PowerBI Report')
|
||||
|
||||
// Update the state "sampleReportConfig" and re-render DemoApp component
|
||||
setReportConfig({
|
||||
...sampleReportConfig,
|
||||
embedUrl: reportConfig.embedUrl,
|
||||
accessToken: reportConfig.embedToken.token
|
||||
});
|
||||
}
|
||||
|
||||
const changeSettings = () => {
|
||||
|
||||
// Update the state "sampleReportConfig" and re-render DemoApp component
|
||||
setReportConfig({
|
||||
...sampleReportConfig,
|
||||
settings: {
|
||||
panes: {
|
||||
filters: {
|
||||
expanded: false,
|
||||
visible: false
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const [displayMessage, setMessage] = useState(`The report is bootstraped. Click 'Embed Report' button below to provide Access Token`);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3>Sample Report:</h3>
|
||||
<PowerBIEmbed
|
||||
embedConfig = { sampleReportConfig }
|
||||
eventHandlers = { eventHandlersMap }
|
||||
cssClassName = { "report-style-class" }
|
||||
getEmbeddedComponent = { (embedObject:Report) => {
|
||||
report = embedObject;
|
||||
console.log(`Embedded object of type "${ report.embedtype }" received`);
|
||||
} }
|
||||
/>
|
||||
<h4>
|
||||
{ displayMessage }
|
||||
</h4>
|
||||
|
||||
<button onClick = { mockSignIn }>
|
||||
Embed Report</button>
|
||||
|
||||
<button onClick = { changeSettings }>
|
||||
Hide filter pane</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DemoApp;
|
|
@ -0,0 +1,7 @@
|
|||
|
||||
<html>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script src="./bundle.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,8 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import DemoApp from './DemoApp';
|
||||
|
||||
ReactDOM.render(
|
||||
<DemoApp/>,
|
||||
document.getElementById('root')
|
||||
);
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"name": "powerbi-client-react-demo",
|
||||
"version": "0.1.0",
|
||||
"description": "Demo for usage of powerbi-client-react",
|
||||
"scripts": {
|
||||
"demo": "webpack-dev-server --content-base ./ --config ./webpack.config.js"
|
||||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"powerbi-client-react": "file:.."
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">= 16.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^16.9.35",
|
||||
"@types/react-dom": "^16.9.8",
|
||||
"css-loader": "^3.5.3",
|
||||
"style-loader": "^1.2.1",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"ts-loader": "^7.0.5",
|
||||
"typescript": "^3.9.3",
|
||||
"webpack": "^4.43.0",
|
||||
"webpack-cli": "^3.3.11",
|
||||
"webpack-dev-server": "^3.11.0"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"include": [
|
||||
"./**/*.tsx",
|
||||
"./**/*.ts"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": false,
|
||||
"noErrorTruncation": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "ES6",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"jsx": "react",
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
let path = require('path');
|
||||
|
||||
module.exports = {
|
||||
mode: 'development',
|
||||
entry: path.resolve('index.tsx'),
|
||||
output: {
|
||||
path: __dirname,
|
||||
filename: 'bundle.js'
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.ts(x)?$/,
|
||||
loader: 'ts-loader'
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: [
|
||||
'style-loader',
|
||||
'css-loader'
|
||||
]
|
||||
},
|
||||
]
|
||||
},
|
||||
resolve: {
|
||||
extensions: [
|
||||
'.tsx',
|
||||
'.ts',
|
||||
'.js',
|
||||
]
|
||||
},
|
||||
devtool: 'source-map',
|
||||
};
|
|
@ -0,0 +1,48 @@
|
|||
{
|
||||
"name": "powerbi-client-react",
|
||||
"version": "0.1.0",
|
||||
"description": "React wrapper for powerbi-client library",
|
||||
"main": "dist/powerbi-client-react.js",
|
||||
"types": "dist/powerbi-client-react.d.ts",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"prebuild": "npm run lint",
|
||||
"build": "webpack --mode=production --config config/src/webpack.config.js",
|
||||
"build:dev": "webpack --mode=development --config config/src/webpack.config.js",
|
||||
"pretest": "webpack --config config/test/webpack.config.js",
|
||||
"test": "karma start config/test/karma.conf.js",
|
||||
"install:demo": "cd demo && npm install",
|
||||
"demo": "cd demo && npm run demo",
|
||||
"lint": "eslint src/**/*.{ts,tsx}"
|
||||
},
|
||||
"keywords": [],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"powerbi-client": "^2.11.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">= 16"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jasmine": "^3.5.10",
|
||||
"@types/node": "^14.0.5",
|
||||
"@types/react": "^16.9.35",
|
||||
"@types/react-dom": "^16.9.8",
|
||||
"@typescript-eslint/eslint-plugin": "^3.1.0",
|
||||
"@typescript-eslint/parser": "^3.0.2",
|
||||
"eslint": "^7.1.0",
|
||||
"eslint-plugin-react": "^7.20.0",
|
||||
"jasmine-core": "^3.5.0",
|
||||
"karma": "^5.0.9",
|
||||
"karma-chrome-launcher": "^3.1.0",
|
||||
"karma-jasmine": "^3.1.1",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"ts-loader": "^7.0.5",
|
||||
"typescript": "^3.9.3",
|
||||
"webpack": "^4.43.0",
|
||||
"webpack-cli": "^3.3.11"
|
||||
}
|
||||
}
|
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 36 KiB |
|
@ -0,0 +1,281 @@
|
|||
import React from "react";
|
||||
import { service, factories, Report, Embed, Dashboard, Tile, Qna, IEmbedConfiguration, Visual } from 'powerbi-client';
|
||||
import { IQnaEmbedConfiguration, IEmbedSettings, IVisualEmbedConfiguration } from "embed";
|
||||
import { stringifyMap } from './utils';
|
||||
|
||||
/**
|
||||
* Props interface for PowerBIEmbed component
|
||||
*/
|
||||
export interface EmbedProps {
|
||||
|
||||
// Configuration for embedding the PowerBI entity
|
||||
embedConfig: IEmbedConfiguration | IQnaEmbedConfiguration | IVisualEmbedConfiguration;
|
||||
|
||||
// Callback method to get the embedded PowerBI entity object (Optional)
|
||||
getEmbeddedComponent?: { (embeddedComponent: Embed): void };
|
||||
|
||||
// Map of pair of event name and its handler method to be triggered on the event (Optional)
|
||||
eventHandlers?: Map<string, service.IEventHandler<any> | null>;
|
||||
|
||||
// CSS class to be set on the embedding container (Optional)
|
||||
cssClassName?: string;
|
||||
|
||||
// Provide a custom implementation of PowerBI service (Optional)
|
||||
service?: service.Service;
|
||||
}
|
||||
|
||||
export enum EmbedType {
|
||||
Report = 'report',
|
||||
Dashboard = 'dashboard',
|
||||
Tile = 'tile',
|
||||
Qna = 'qna',
|
||||
Visual = 'visual'
|
||||
}
|
||||
|
||||
/**
|
||||
* Base react component to embed Power BI entities like: reports, dashboards, tiles, visual and qna containers.
|
||||
*/
|
||||
export class PowerBIEmbed extends React.Component<EmbedProps> {
|
||||
|
||||
// Embedded entity
|
||||
private embed?: Embed;
|
||||
|
||||
// Powerbi service
|
||||
private powerbi: service.Service;
|
||||
|
||||
// Ref to the HTML div element
|
||||
private containerRef = React.createRef<HTMLDivElement>();
|
||||
|
||||
// JSON stringify of prev event handler map
|
||||
private prevEventHandlerMapString = '';
|
||||
|
||||
constructor(props: EmbedProps) {
|
||||
super(props);
|
||||
|
||||
if (this.props.service) {
|
||||
this.powerbi = this.props.service;
|
||||
}
|
||||
else {
|
||||
this.powerbi = new service.Service(
|
||||
factories.hpmFactory,
|
||||
factories.wpmpFactory,
|
||||
factories.routerFactory);
|
||||
}
|
||||
};
|
||||
|
||||
componentDidMount(): void {
|
||||
|
||||
// Check if HTML container is available
|
||||
if (this.containerRef.current) {
|
||||
|
||||
// Decide to bootstrap or embed
|
||||
if (this.props.embedConfig.accessToken && this.props.embedConfig.embedUrl) {
|
||||
this.embed = this.powerbi.embed(this.containerRef.current, this.props.embedConfig);
|
||||
}
|
||||
else {
|
||||
this.embed = this.powerbi.bootstrap(this.containerRef.current, this.props.embedConfig);
|
||||
}
|
||||
}
|
||||
|
||||
// Invoke callback method in Props
|
||||
this.getEmbedCallback();
|
||||
|
||||
// Set event handlers if available
|
||||
if (this.props.eventHandlers && this.embed) {
|
||||
this.setEventHandlers(this.embed, this.props.eventHandlers);
|
||||
}
|
||||
};
|
||||
|
||||
componentDidUpdate(prevProps: EmbedProps): void {
|
||||
|
||||
this.embedOrUpdateAccessToken(prevProps);
|
||||
|
||||
// Set event handlers if available
|
||||
if (this.props.eventHandlers && this.embed) {
|
||||
this.setEventHandlers(this.embed, this.props.eventHandlers);
|
||||
}
|
||||
|
||||
// Update settings in embedConfig of props
|
||||
this.updateSettings();
|
||||
};
|
||||
|
||||
componentWillUnmount(): void {
|
||||
// Clean Up
|
||||
if (this.containerRef.current) {
|
||||
this.powerbi.reset(this.containerRef.current);
|
||||
}
|
||||
};
|
||||
|
||||
render(): JSX.Element {
|
||||
return (
|
||||
<div
|
||||
ref={this.containerRef}
|
||||
className={this.props.cssClassName}>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
/**
|
||||
* Choose to _embed_ the powerbi entity or _update the accessToken_ in the embedded entity
|
||||
* or do nothing when the embedUrl and accessToken did not update in the new props
|
||||
*
|
||||
* @param prevProps EmbedProps
|
||||
* @returns void
|
||||
*/
|
||||
private embedOrUpdateAccessToken(prevProps: EmbedProps): void {
|
||||
|
||||
// Check if Embed URL and Access Token are present in current props
|
||||
if (!this.props.embedConfig.accessToken || !this.props.embedConfig.embedUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Embed in the following scenarios
|
||||
// 1. AccessToken was not provided in prev props (E.g. Report was bootstrapped earlier)
|
||||
// 2. Embed URL is updated (E.g. New report is to be embedded)
|
||||
if (this.containerRef.current
|
||||
&& (!prevProps.embedConfig.accessToken
|
||||
|| this.props.embedConfig.embedUrl !== prevProps.embedConfig.embedUrl)) {
|
||||
this.embed = this.powerbi.embed(this.containerRef.current, this.props.embedConfig);
|
||||
}
|
||||
|
||||
// Set new access token,
|
||||
// when access token is updated but embed Url is same
|
||||
else if (this.props.embedConfig.accessToken !== prevProps.embedConfig.accessToken
|
||||
&& this.props.embedConfig.embedUrl === prevProps.embedConfig.embedUrl
|
||||
&& this.embed) {
|
||||
|
||||
this.embed.setAccessToken(this.props.embedConfig.accessToken)
|
||||
.catch(error => {
|
||||
console.error(`setAccessToken error: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Invoke callback method in Props
|
||||
this.getEmbedCallback();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets all event handlers from the props on the embedded entity
|
||||
*
|
||||
* @param embed Embedded object
|
||||
* @param eventHandlers Array of eventhandlers to be set on embedded entity
|
||||
* @returns void
|
||||
*/
|
||||
private setEventHandlers(
|
||||
embed: Embed,
|
||||
eventHandlerMap: Map<string, service.IEventHandler<any> | null>): void {
|
||||
|
||||
// Get string representation of eventHandlerMap
|
||||
const eventHandlerMapString = stringifyMap(this.props.eventHandlers);
|
||||
|
||||
// Check if event handler map changed
|
||||
if (this.prevEventHandlerMapString === eventHandlerMapString) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update prev string representation of event handler map
|
||||
this.prevEventHandlerMapString = eventHandlerMapString;
|
||||
|
||||
// List of allowed events
|
||||
let allowedEvents = Embed.allowedEvents;
|
||||
|
||||
const entityType = embed.embedtype;
|
||||
|
||||
// Append entity specific events
|
||||
switch (entityType) {
|
||||
case EmbedType.Report:
|
||||
allowedEvents = [...allowedEvents, ...Report.allowedEvents]
|
||||
break;
|
||||
case EmbedType.Dashboard:
|
||||
allowedEvents = [...allowedEvents, ...Dashboard.allowedEvents]
|
||||
break;
|
||||
case EmbedType.Tile:
|
||||
allowedEvents = [...allowedEvents, ...Tile.allowedEvents]
|
||||
break;
|
||||
case EmbedType.Qna:
|
||||
allowedEvents = [...allowedEvents, ...Qna.allowedEvents]
|
||||
break;
|
||||
case EmbedType.Visual:
|
||||
allowedEvents = [...allowedEvents, ...Visual.allowedEvents]
|
||||
break;
|
||||
default:
|
||||
console.error(`Invalid embed type ${entityType}`);
|
||||
}
|
||||
|
||||
// Holds list of events which are not allowed
|
||||
const invalidEvents: Array<string> = [];
|
||||
|
||||
// Apply all provided event handlers
|
||||
eventHandlerMap.forEach(function (eventHandlerMethod, eventName) {
|
||||
|
||||
// Check if this event is allowed
|
||||
if (allowedEvents.includes(eventName)) {
|
||||
|
||||
// Removes event handler for this event
|
||||
embed.off(eventName);
|
||||
|
||||
if (eventHandlerMethod) {
|
||||
|
||||
// Set single event handler
|
||||
embed.on(eventName, eventHandlerMethod);
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
||||
// Add this event name to the list of invalid events
|
||||
invalidEvents.push(eventName);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle invalid events
|
||||
if (invalidEvents.length) {
|
||||
console.error(`Following events are invalid: ${invalidEvents.join(',')}`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the embedded object via _getEmbed_ callback method provided in props
|
||||
*
|
||||
* @returns void
|
||||
*/
|
||||
private getEmbedCallback(): void {
|
||||
if (this.props.getEmbeddedComponent && this.embed) {
|
||||
this.props.getEmbeddedComponent(this.embed);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Update settings from props of the embedded artifact
|
||||
*
|
||||
* @returns void
|
||||
*/
|
||||
private updateSettings(): void {
|
||||
if (!this.embed || !this.props.embedConfig.settings) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (this.props.embedConfig.type) {
|
||||
case EmbedType.Report:
|
||||
|
||||
// Typecasted to IEmbedSettings as props.embedConfig.settings can be ISettings via IQnaEmbedConfiguration
|
||||
const settings = this.props.embedConfig.settings as IEmbedSettings;
|
||||
|
||||
// Upcast to Report and call updateSettings
|
||||
(this.embed as Report).updateSettings(settings)
|
||||
.catch((error: any) => {
|
||||
console.error(`Error in method updateSettings: ${error}`);
|
||||
});
|
||||
break;
|
||||
|
||||
case EmbedType.Dashboard:
|
||||
case EmbedType.Tile:
|
||||
case EmbedType.Qna:
|
||||
case EmbedType.Visual:
|
||||
// updateSettings not applicable for these embedding types
|
||||
break;
|
||||
|
||||
default:
|
||||
console.error(`Invalid embed type ${this.props.embedConfig.type}`);
|
||||
}
|
||||
};
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
export {
|
||||
PowerBIEmbed,
|
||||
EmbedProps,
|
||||
EmbedType
|
||||
} from './PowerBIEmbed'
|
|
@ -0,0 +1,40 @@
|
|||
import { EmbedProps } from "./PowerBIEmbed";
|
||||
|
||||
/**
|
||||
* Get JSON string representation of the given map.
|
||||
*
|
||||
* @param map Map of event and corresponding handler method
|
||||
*
|
||||
* For example:
|
||||
* Input:
|
||||
* ```
|
||||
* Map([
|
||||
['loaded', null],
|
||||
['rendered', function () { console.log('Rendered'); }]
|
||||
]);
|
||||
* ```
|
||||
* Output:
|
||||
* ```
|
||||
* `[["loaded",""],["rendered","function () { console.log('Rendered'); }"]]`
|
||||
* ```
|
||||
*/
|
||||
export function stringifyMap(map: EmbedProps['eventHandlers']): string {
|
||||
|
||||
// Return empty string for empty/null map
|
||||
if (!map) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Get entries of map as array
|
||||
const mapEntries = Array.from(map);
|
||||
|
||||
// Return JSON string
|
||||
return JSON.stringify(mapEntries.map((mapEntry) => {
|
||||
|
||||
// Convert event handler method to a string containing its source code for comparison
|
||||
return [
|
||||
mapEntry[0],
|
||||
mapEntry[1] ? mapEntry[1].toString() : ''
|
||||
];
|
||||
}));
|
||||
};
|
|
@ -0,0 +1,569 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { act, isElement } from 'react-dom/test-utils';
|
||||
import { Report, Dashboard, service, factories} from 'powerbi-client';
|
||||
import { PowerBIEmbed } from '../src/PowerBIEmbed';
|
||||
import { mockPowerBIService, mockedMethods } from "./mockService";
|
||||
|
||||
describe('tests of PowerBIEmbed', function () {
|
||||
|
||||
let container: HTMLDivElement | null;
|
||||
|
||||
beforeEach(function () {
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
|
||||
// Reset all methods in PowerBI Service spy object
|
||||
mockedMethods.forEach(mockedMethod => {
|
||||
mockPowerBIService[mockedMethod].calls.reset();
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
if (container){
|
||||
document.body.removeChild(container);
|
||||
container = null;
|
||||
}
|
||||
});
|
||||
|
||||
describe('basic tests', function () {
|
||||
|
||||
it('is a react component', () => {
|
||||
const component = <PowerBIEmbed embedConfig={{ type: 'report' }} />
|
||||
expect(isElement(component)).toBe(true);
|
||||
});
|
||||
|
||||
it('renders exactly one div', () => {
|
||||
act(() => {
|
||||
ReactDOM.render(<PowerBIEmbed embedConfig = {{type: 'report'}}/>, container);
|
||||
});
|
||||
|
||||
const divCount = container.querySelectorAll('div').length;
|
||||
expect(divCount).toBe(1);
|
||||
});
|
||||
|
||||
it('renders exactly one iframe', () => {
|
||||
act(() => {
|
||||
ReactDOM.render(<PowerBIEmbed embedConfig = {{type: 'report'}}/>, container);
|
||||
});
|
||||
|
||||
const divCount = container.querySelectorAll('iframe').length;
|
||||
expect(divCount).toBe(1);
|
||||
});
|
||||
|
||||
it('sets the css classes', () => {
|
||||
const inputCssClass = 'test-class another-test-class';
|
||||
|
||||
act(() => {
|
||||
ReactDOM.render(
|
||||
<PowerBIEmbed
|
||||
embedConfig = {{type: 'report'}}
|
||||
cssClassName = {inputCssClass}/>
|
||||
, container);
|
||||
});
|
||||
|
||||
const divClass = container.querySelectorAll('div')[0].className;
|
||||
expect(divClass).toBe(inputCssClass);
|
||||
});
|
||||
|
||||
it('gets the embedded report object', () => {
|
||||
let testReport = undefined;
|
||||
|
||||
act(() => {
|
||||
|
||||
ReactDOM.render(
|
||||
<PowerBIEmbed
|
||||
embedConfig={{ type: 'report' }}
|
||||
getEmbeddedComponent={(callbackReport) => {
|
||||
testReport = callbackReport;
|
||||
}}
|
||||
/>, container);
|
||||
});
|
||||
|
||||
expect(testReport).not.toBe(undefined);
|
||||
expect(testReport instanceof Report).toBe(true);
|
||||
});
|
||||
|
||||
it('gets the embedded dashboard object', () => {
|
||||
let testReport = undefined;
|
||||
|
||||
act(() => {
|
||||
|
||||
ReactDOM.render(
|
||||
<PowerBIEmbed
|
||||
embedConfig={{ type: 'dashboard' }}
|
||||
getEmbeddedComponent={(callbackReport) => {
|
||||
testReport = callbackReport;
|
||||
}}
|
||||
/>, container);
|
||||
});
|
||||
|
||||
expect(testReport).not.toBe(undefined);
|
||||
expect(testReport instanceof Dashboard).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('updates powerbi report settings', () => {
|
||||
let testReport: Report = undefined;
|
||||
|
||||
act(() => {
|
||||
|
||||
ReactDOM.render(
|
||||
<PowerBIEmbed
|
||||
embedConfig={{ type: 'report' }}
|
||||
getEmbeddedComponent={(callbackReport: Report) => {
|
||||
testReport = callbackReport;
|
||||
}}
|
||||
/>, container);
|
||||
});
|
||||
|
||||
spyOn(testReport, 'updateSettings').and.callThrough();
|
||||
|
||||
// Update settings via props
|
||||
act(() => {
|
||||
|
||||
ReactDOM.render(
|
||||
<PowerBIEmbed
|
||||
embedConfig={{ type: 'report', settings: { filterPaneEnabled: false } }}
|
||||
getEmbeddedComponent={(callbackReport: Report) => {
|
||||
testReport = callbackReport;
|
||||
}}
|
||||
/>, container);
|
||||
});
|
||||
|
||||
expect(testReport.updateSettings).toHaveBeenCalledTimes(1);
|
||||
expect(testReport.updateSettings).toHaveBeenCalledWith({ filterPaneEnabled: false });
|
||||
});
|
||||
|
||||
it('sets new token received in updated props (case: Token expired)', () => {
|
||||
|
||||
// Arrange
|
||||
let testReport:Report = undefined;
|
||||
let config = {
|
||||
type: 'report',
|
||||
id: 'fakeId',
|
||||
embedUrl: 'fakeUrl',
|
||||
accessToken: 'fakeToken'
|
||||
};
|
||||
|
||||
// New accessToken
|
||||
let newConfig = {
|
||||
type: 'report',
|
||||
id: 'fakeId',
|
||||
embedUrl: 'fakeUrl',
|
||||
accessToken: 'newfakeToken'
|
||||
};
|
||||
|
||||
act(() => {
|
||||
|
||||
ReactDOM.render(
|
||||
<PowerBIEmbed
|
||||
embedConfig = { config }
|
||||
getEmbeddedComponent = {(callbackReport:Report) => {
|
||||
testReport = callbackReport;
|
||||
}}
|
||||
/>, container);
|
||||
});
|
||||
|
||||
spyOn(testReport, 'setAccessToken').and.callThrough();
|
||||
|
||||
// Act
|
||||
// Update accessToken via props
|
||||
act(() => {
|
||||
|
||||
ReactDOM.render(
|
||||
<PowerBIEmbed
|
||||
embedConfig = { newConfig }
|
||||
getEmbeddedComponent = {(callbackReport:Report) => {
|
||||
testReport = callbackReport;
|
||||
}}
|
||||
/>, container);
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(testReport).toBeDefined();
|
||||
expect(testReport.setAccessToken).toHaveBeenCalledTimes(1);
|
||||
expect(testReport.setAccessToken).toHaveBeenCalledWith(newConfig.accessToken);
|
||||
});
|
||||
|
||||
describe('test powerbi service interaction',() => {
|
||||
|
||||
it('embeds report when accessToken provided', () => {
|
||||
let config = {
|
||||
type: 'report',
|
||||
id: 'fakeId',
|
||||
embedUrl: 'fakeUrl',
|
||||
accessToken: 'fakeToken'
|
||||
};
|
||||
|
||||
act(() => {
|
||||
|
||||
ReactDOM.render(
|
||||
<PowerBIEmbed
|
||||
embedConfig = { config }
|
||||
service = { mockPowerBIService }
|
||||
/>, container);
|
||||
});
|
||||
|
||||
expect(mockPowerBIService.bootstrap).toHaveBeenCalledTimes(0);
|
||||
expect(mockPowerBIService.embed).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('bootstraps report when no accessToken provided', () => {
|
||||
let config = {
|
||||
type: 'report',
|
||||
id: 'fakeId',
|
||||
embedUrl: 'fakeUrl'
|
||||
};
|
||||
|
||||
act(() => {
|
||||
|
||||
ReactDOM.render(
|
||||
<PowerBIEmbed
|
||||
embedConfig = { config }
|
||||
service = { mockPowerBIService }
|
||||
/>, container);
|
||||
});
|
||||
|
||||
expect(mockPowerBIService.embed).toHaveBeenCalledTimes(0);
|
||||
expect(mockPowerBIService.bootstrap).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('first bootstraps, then embeds when accessToken is available', () => {
|
||||
|
||||
// Arrange
|
||||
const config = {
|
||||
type: 'report',
|
||||
id: 'fakeId',
|
||||
embedUrl: 'fakeUrl',
|
||||
accessToken: null
|
||||
};
|
||||
const newConfig = {
|
||||
type: 'report',
|
||||
id: 'fakeId',
|
||||
embedUrl: 'fakeUrl',
|
||||
accessToken: 'fakeToken'
|
||||
};
|
||||
|
||||
// Act
|
||||
// Without accessToken (bootstrap)
|
||||
act(() => {
|
||||
|
||||
ReactDOM.render(
|
||||
<PowerBIEmbed
|
||||
embedConfig = { config }
|
||||
service = { mockPowerBIService }
|
||||
/>, container);
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(mockPowerBIService.embed).toHaveBeenCalledTimes(0);
|
||||
expect(mockPowerBIService.bootstrap).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Reset for next Act
|
||||
mockPowerBIService.embed.calls.reset();
|
||||
mockPowerBIService.bootstrap.calls.reset();
|
||||
|
||||
// With accessToken (embed)
|
||||
act(() => {
|
||||
|
||||
ReactDOM.render(
|
||||
<PowerBIEmbed
|
||||
embedConfig = { newConfig }
|
||||
service = { mockPowerBIService }
|
||||
/>, container);
|
||||
});
|
||||
|
||||
expect(mockPowerBIService.bootstrap).toHaveBeenCalledTimes(0);
|
||||
expect(mockPowerBIService.embed).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('does not embed again when accessToken and embedUrl are same', () => {
|
||||
const config = {
|
||||
type: 'report',
|
||||
id: 'fakeId',
|
||||
embedUrl: 'fakeUrl',
|
||||
accessToken: 'fakeToken',
|
||||
};
|
||||
const newConfig = {
|
||||
type: 'report',
|
||||
id: 'fakeId',
|
||||
embedUrl: 'fakeUrl',
|
||||
accessToken: 'fakeToken',
|
||||
settings: { filterPaneEnabled: false }
|
||||
};
|
||||
|
||||
act(() => {
|
||||
|
||||
ReactDOM.render(
|
||||
<PowerBIEmbed
|
||||
embedConfig = { config }
|
||||
service = { mockPowerBIService }
|
||||
/>, container);
|
||||
});
|
||||
|
||||
expect(mockPowerBIService.embed).toHaveBeenCalledTimes(1);
|
||||
mockPowerBIService.embed.calls.reset();
|
||||
|
||||
// With accessToken (embed)
|
||||
act(() => {
|
||||
|
||||
ReactDOM.render(
|
||||
<PowerBIEmbed
|
||||
embedConfig = { newConfig }
|
||||
service = { mockPowerBIService }
|
||||
/>, container);
|
||||
});
|
||||
|
||||
expect(mockPowerBIService.embed).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('powerbi.reset called when component unmounts', () => {
|
||||
let config = {
|
||||
type: 'report',
|
||||
id: 'fakeId',
|
||||
embedUrl: 'fakeUrl',
|
||||
accessToken: 'fakeToken'
|
||||
};
|
||||
|
||||
act(() => {
|
||||
|
||||
ReactDOM.render(
|
||||
<PowerBIEmbed
|
||||
embedConfig = { config }
|
||||
service = { mockPowerBIService }
|
||||
/>, container);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
|
||||
ReactDOM.unmountComponentAtNode(container);
|
||||
});
|
||||
|
||||
expect(mockPowerBIService.reset).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("new report's url in updated props", () => {
|
||||
let config = {
|
||||
type: 'report',
|
||||
id: 'fakeId',
|
||||
embedUrl: 'fakeUrl',
|
||||
accessToken: 'fakeToken'
|
||||
};
|
||||
|
||||
act(() => {
|
||||
|
||||
ReactDOM.render(
|
||||
<PowerBIEmbed
|
||||
embedConfig = { config }
|
||||
service = { mockPowerBIService }
|
||||
/>, container);
|
||||
});
|
||||
|
||||
config.embedUrl = 'newFakeUrl';
|
||||
|
||||
act(() => {
|
||||
|
||||
ReactDOM.render(
|
||||
<PowerBIEmbed
|
||||
embedConfig = { config }
|
||||
service = { mockPowerBIService }
|
||||
/>, container);
|
||||
});
|
||||
|
||||
expect(mockPowerBIService.embed).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('tests for setting event handlers', () => {
|
||||
it('clears and sets the event handlers', () => {
|
||||
// Arrange
|
||||
let testReport:Report = undefined;
|
||||
let eventHandlers = new Map([
|
||||
['loaded', function () { }],
|
||||
['rendered', function () { }],
|
||||
['error', function () { }]
|
||||
]);
|
||||
|
||||
// Initialise testReport
|
||||
act(() => {
|
||||
|
||||
ReactDOM.render(
|
||||
<PowerBIEmbed
|
||||
embedConfig = { {type:'report'} }
|
||||
getEmbeddedComponent = {(callbackReport:Report) => {
|
||||
testReport = callbackReport;
|
||||
}}
|
||||
/>, container);
|
||||
});
|
||||
|
||||
spyOn(testReport, 'off');
|
||||
spyOn(testReport, 'on');
|
||||
|
||||
// Act
|
||||
act(() => {
|
||||
|
||||
ReactDOM.render(
|
||||
<PowerBIEmbed
|
||||
embedConfig = { {type:'report'} }
|
||||
eventHandlers = { eventHandlers }
|
||||
/>, container);
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(testReport.off).toHaveBeenCalledTimes(eventHandlers.size);
|
||||
expect(testReport.on).toHaveBeenCalledTimes(eventHandlers.size);
|
||||
});
|
||||
|
||||
it('clears the already set event handlers in case of null provided for handler', () => {
|
||||
// Arrange
|
||||
let testReport:Report = undefined;
|
||||
const eventHandlers = new Map([
|
||||
['loaded', function () { }],
|
||||
['rendered', function () { }],
|
||||
['error', function () { }]
|
||||
]);
|
||||
const newEventHandlers = new Map([
|
||||
['loaded', null],
|
||||
['rendered', null],
|
||||
['error', function () { }]
|
||||
]);
|
||||
|
||||
// Initialise testReport
|
||||
act(() => {
|
||||
ReactDOM.render(
|
||||
<PowerBIEmbed
|
||||
embedConfig = { {type:'report'} }
|
||||
getEmbeddedComponent = {(callbackReport:Report) => {
|
||||
testReport = callbackReport;
|
||||
}}
|
||||
/>, container);
|
||||
});
|
||||
|
||||
spyOn(testReport, 'off');
|
||||
spyOn(testReport, 'on');
|
||||
|
||||
// Act
|
||||
act(() => {
|
||||
ReactDOM.render(
|
||||
<PowerBIEmbed
|
||||
embedConfig = { {type:'report'} }
|
||||
eventHandlers = { newEventHandlers }
|
||||
/>, container);
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(testReport.off).toHaveBeenCalledTimes(eventHandlers.size);
|
||||
// Two events are turned off in new eventhandlers
|
||||
expect(testReport.on).toHaveBeenCalledTimes(eventHandlers.size - 2);
|
||||
});
|
||||
|
||||
it('does not console error for valid events for report', () => {
|
||||
const eventHandlers = new Map([
|
||||
['loaded', function () { }],
|
||||
['saved', function () { }],
|
||||
['rendered', function () { }],
|
||||
['saveAsTriggered', function () { }],
|
||||
['dataSelected', function () { }],
|
||||
['buttonClicked', function () { }],
|
||||
['filtersApplied', function () { }],
|
||||
['pageChanged', function () { }],
|
||||
['commandTriggered', function () { }],
|
||||
['swipeStart', function () { }],
|
||||
['swipeEnd', function () { }],
|
||||
['bookmarkApplied', function () { }],
|
||||
['dataHyperlinkClicked', function () { }],
|
||||
['error', function () { }]
|
||||
]);
|
||||
|
||||
spyOn(console, 'error');
|
||||
|
||||
act(() => {
|
||||
ReactDOM.render(
|
||||
<PowerBIEmbed
|
||||
embedConfig={{ type: 'report' }}
|
||||
eventHandlers={eventHandlers}
|
||||
/>, container);
|
||||
});
|
||||
|
||||
expect(console.error).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('consoles error for invalid events', () => {
|
||||
// Arrange
|
||||
const invalidEvent1 = 'invalidEvent1';
|
||||
const invalidEvent2 = 'invalidEvent2';
|
||||
const errorMessage = `Following events are invalid: ${invalidEvent1},${invalidEvent2}`;
|
||||
|
||||
const eventHandlers = new Map([
|
||||
[invalidEvent1, function () { }],
|
||||
['rendered', function () { }],
|
||||
['error', function () { }],
|
||||
[invalidEvent2, function () { }],
|
||||
]);
|
||||
|
||||
const powerbi = new service.Service(
|
||||
factories.hpmFactory,
|
||||
factories.wpmpFactory,
|
||||
factories.routerFactory);
|
||||
const embed = powerbi.bootstrap(container, {type:'tile'});
|
||||
|
||||
spyOn(console, 'error');
|
||||
|
||||
// Act
|
||||
const powerbiembed = new PowerBIEmbed({
|
||||
embedConfig: { type: 'tile' },
|
||||
eventHandlers: eventHandlers
|
||||
});
|
||||
|
||||
// Ignoring next line as setEventHandlers is a private method
|
||||
// @ts-ignore
|
||||
powerbiembed.setEventHandlers(embed, eventHandlers);
|
||||
|
||||
expect(console.error).toHaveBeenCalledWith(errorMessage);
|
||||
});
|
||||
|
||||
it('does not set the same eventhandler map again', () => {
|
||||
// Arrange
|
||||
let testReport:Report = undefined;
|
||||
const eventHandlers = new Map([
|
||||
['loaded', function () { }],
|
||||
['rendered', function () { }],
|
||||
['error', function () { }]
|
||||
]);
|
||||
const newSameEventHandlers = new Map([
|
||||
['loaded', function () { }],
|
||||
['rendered', function () { }],
|
||||
['error', function () { }]
|
||||
]);
|
||||
|
||||
// Initialise testReport
|
||||
act(() => {
|
||||
ReactDOM.render(
|
||||
<PowerBIEmbed
|
||||
embedConfig = { {type:'report'} }
|
||||
getEmbeddedComponent = {(callbackReport:Report) => {
|
||||
testReport = callbackReport;
|
||||
}}
|
||||
eventHandlers = { eventHandlers }
|
||||
/>, container);
|
||||
});
|
||||
|
||||
spyOn(testReport, 'off');
|
||||
spyOn(testReport, 'on');
|
||||
|
||||
// Act
|
||||
act(() => {
|
||||
ReactDOM.render(
|
||||
<PowerBIEmbed
|
||||
embedConfig = { {type:'report'} }
|
||||
eventHandlers = { newSameEventHandlers }
|
||||
/>, container);
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(testReport.off).not.toHaveBeenCalled();
|
||||
expect(testReport.on).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,5 @@
|
|||
const mockedMethods = ['init', 'embed', 'bootstrap', 'load', 'get', 'reset', 'preload'];
|
||||
|
||||
const mockPowerBIService = jasmine.createSpyObj('mockService', mockedMethods);
|
||||
|
||||
export { mockPowerBIService, mockedMethods };
|
|
@ -0,0 +1,68 @@
|
|||
import { service } from 'powerbi-client';
|
||||
import { stringifyMap } from '../src/utils';
|
||||
|
||||
describe('tests of PowerBIEmbed', function () {
|
||||
|
||||
let container: HTMLDivElement | null;
|
||||
|
||||
beforeEach(function () {
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
if (container){
|
||||
document.body.removeChild(container);
|
||||
container = null;
|
||||
}
|
||||
});
|
||||
|
||||
// Tests for utils stringifyMap
|
||||
describe('tests PowerBIEmbed stringifyMap method', () => {
|
||||
|
||||
it('stringifies the event handler map', () => {
|
||||
|
||||
// Arrange
|
||||
const eventHandlerMap = new Map([
|
||||
['loaded', function () { console.log('Report loaded'); }],
|
||||
['rendered', function () { console.log('Rendered'); }]
|
||||
]);
|
||||
const expectedString = `[["loaded","function () { console.log('Report loaded'); }"],["rendered","function () { console.log('Rendered'); }"]]`;
|
||||
|
||||
// Act
|
||||
const jsonStringOutput = stringifyMap(eventHandlerMap);
|
||||
|
||||
// Assert
|
||||
expect(jsonStringOutput).toBe(expectedString);
|
||||
});
|
||||
|
||||
it('stringifies empty event handler map', () => {
|
||||
|
||||
// Arrange
|
||||
const eventHandlerMap = new Map<string, service.IEventHandler<any>>([]);
|
||||
const expectedString = `[]`;
|
||||
|
||||
// Act
|
||||
const jsonStringOutput = stringifyMap(eventHandlerMap);
|
||||
|
||||
// Assert
|
||||
expect(jsonStringOutput).toBe(expectedString);
|
||||
});
|
||||
|
||||
it('stringifies null in event handler map', () => {
|
||||
|
||||
// Arrange
|
||||
const eventHandlerMap = new Map([
|
||||
['loaded', null],
|
||||
['rendered', function () { console.log('Rendered'); }]
|
||||
]);
|
||||
const expectedString = `[["loaded",""],["rendered","function () { console.log('Rendered'); }"]]`;
|
||||
|
||||
// Act
|
||||
const jsonStringOutput = stringifyMap(eventHandlerMap);
|
||||
|
||||
// Assert
|
||||
expect(jsonStringOutput).toBe(expectedString);
|
||||
});
|
||||
});
|
||||
});
|
Загрузка…
Ссылка в новой задаче