Merge remote-tracking branch 'powerbi/master' into release-1-4-0

This commit is contained in:
Karan Dewani 2023-05-04 17:34:41 +05:30
Родитель de0d0ece26 80d692a23e
Коммит f0e97cd40a
37 изменённых файлов: 16121 добавлений и 1114 удалений

7
.gitignore поставляемый
Просмотреть файл

@ -3,14 +3,13 @@
/.pnp
.pnp.js
**/package-lock.json
**/.pipelines
# testing
/coverage
/compiledTests
coverage
compiledTests
# production
/dist
dist
# misc
.DS_Store

Просмотреть файл

@ -9,6 +9,11 @@ git clone <url>
Navigate to the cloned directory
Navigate to the React\powerbi-client-react workspace folder:
```
cd React\powerbi-client-react
```
Install local dependencies:
```
npm install
@ -24,14 +29,13 @@ Or if using VScode: `Ctrl + Shift + B`
```
npm test
```
By default the tests run using PhantomJS browser
By default the tests run using ChromeHeadless browser
The build and tests use webpack to compile all the source modules into bundled module that can be executed in the browser.
## Running the demo
```
npm run install:demo
npm run demo
```
@ -40,4 +44,4 @@ Open the address to view in the browser:
http://localhost:8080/
## Flow Diagram for the PowerBIEmbed Component:
![Flow Diagram](https://github.com/microsoft/powerbi-client-react/raw/master/resources/react_wrapper_flow_diagram.png)
![Flow Diagram](/resources/react_wrapper_flow_diagram.png)

Просмотреть файл

@ -1,3 +1,5 @@
powerbi-client-react
Copyright (c) Microsoft Corporation.
MIT License

124
README.md
Просмотреть файл

@ -1,5 +1,5 @@
# powerbi-client-react
Power BI React component. This library lets you embed Power BI report, dashboard, dashboard tile, report visual, or Q&A in your React application.
Power BI React component. This library enables you to embed Power BI reports, dashboards, dashboard tiles, report visuals, Q&A or paginated reports in your React application, and to create new Power BI reports directly in your application.
## Quick Start
@ -13,11 +13,11 @@ import { PowerBIEmbed } from 'powerbi-client-react';
```jsx
<PowerBIEmbed
embedConfig = {{
type: 'report', // Supported types: report, dashboard, tile, visual and qna
type: 'report', // Supported types: report, dashboard, tile, visual, qna, paginated report and create
id: '<Report Id>',
embedUrl: '<Embed Url>',
accessToken: '<Access Token>',
tokenType: models.TokenType.Embed,
tokenType: models.TokenType.Embed, // Use models.TokenType.Aad for SaaS embed
settings: {
panes: {
filters: {
@ -29,15 +29,17 @@ import { PowerBIEmbed } from 'powerbi-client-react';
}
}}
eventHandlers = {
eventHandlers = {
new Map([
['loaded', function () {console.log('Report loaded');}],
['rendered', function () {console.log('Report rendered');}],
['error', function (event) {console.log(event.detail);}]
['error', function (event) {console.log(event.detail);}],
['visualClicked', () => console.log('visual clicked')],
['pageChanged', (event) => console.log(event)],
])
}
cssClassName = { "report-style-class" }
cssClassName = { "reportClass" }
getEmbeddedComponent = { (embeddedReport) => {
this.report = embeddedReport as Report;
@ -45,30 +47,31 @@ import { PowerBIEmbed } from 'powerbi-client-react';
/>
```
### How to [bootstrap a PowerBI report](https://aka.ms/PbieBootstrap):
### How to [bootstrap a PowerBI report](https://learn.microsoft.com/javascript/api/overview/powerbi/bootstrap-better-performance):
```jsx
<PowerBIEmbed
embedConfig = {{
type: 'report', // Supported types: report, dashboard, tile, visual and qna
id: undefined,
type: 'report', // Supported types: report, dashboard, tile, visual, qna and paginated report
id: undefined,
embedUrl: undefined,
accessToken: undefined, // Keep as empty string, null or undefined
tokenType: models.TokenType.Embed
}}
/>
```
__Note__: To embed the report after bootstrap, update the props (with atleast accessToken).
__Note__: To embed the report after bootstrap, update the props (with at least accessToken).
### Demo
A React application that embeds a sample report using the _PowerBIEmbed_ component.<br/>
It demonstrates the complete flow from bootstrapping the report, to embedding and updating the embedded report.<br/>
It also demonstrates the usage of _powerbi report authoring_ library by deleting a visual from report on click of "Delete a Visual" button.
This demo includes a React application that demonstrates the complete flow of embedding a sample report using the PowerBIEmbed component.
The demo shows how to bootstrap the report, embed it, and update it. Additionally, the demo showcases the usage of the powerbi report authoring library by enabling the user to change the type of visual from a report using the "Change visual type" button.
The demo also sets a "DataSelected" event, which allows the user to interact with the embedded report and retrieve information about the selected data.
To run the demo on localhost, run the following commands:
```
npm run install:demo
npm run demo
```
@ -84,11 +87,12 @@ Redirect to http://localhost:8080/ to view in the browser.
|Reset event handlers|To reset event handler for an event, set the event handler's value as `null` in the _eventHandlers_ map of props.|
|Set new accessToken|To set new accessToken in the same embedded powerbi artifact, pass the updated _accessToken_ in _embedConfig_ of props. <br/>Reload manually with report.reload() after providing new token if the current token in report has already expired<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 [Quick Start](#quick-start).<br/>__Note__: Update the settings only by updating embedConfig prop|
|Bootstrap Power BI|To [bootstrap your powerbi entity](https://aka.ms/PbieBootstrap), pass the props to the component without _accessToken_ in _embedConfig_.<br/>__Note__: _embedConfig_ of props should atleast contain __type__ of the powerbi entity being embedded. <br/>Available types: "report", "dashboard", "tile", "visual" and "qna".<br/>Refer to _How to bootstrap a report_ section in [Quick Start](#quick-start).|
|Bootstrap Power BI|To [bootstrap your powerbi entity](https://learn.microsoft.com/javascript/api/overview/powerbi/bootstrap-better-performance), pass the props to the component without _accessToken_ in _embedConfig_.<br/>__Note__: _embedConfig_ of props should at least contain __type__ of the powerbi entity being embedded. <br/>Available types: "report", "dashboard", "tile", "visual", "qna" and "paginated report".<br/>Refer to _How to bootstrap a report_ section in [Quick Start](#quick-start).|
|Using with PowerBI Report Authoring|1. Install [powerbi-report-authoring](https://www.npmjs.com/package/powerbi-report-authoring) as npm dependency.<br>2. Use the report authoring APIs using the embedded report's instance|
|Phased embedding (Report type only)|Set phasedEmbedding prop's value as `true` <br/> Refer to [Phased embedding docs](https://github.com/microsoft/PowerBI-JavaScript/wiki/Phased-Embedding).|
|Phased embedding (Report type only)|Set phasedEmbedding prop's value as `true` <br/> Refer to [Phased embedding docs](https://learn.microsoft.com/javascript/api/overview/powerbi/phased-embedding).|
|Apply Filters (Report type only)|1. To apply updated filters, update filters in _embedConfig_ props.<br/>2. To remove the applied filters, update the _embedConfig_ prop with the filters removed or set as undefined/null.|
|Set Page (Report type only)|To set a page when embedding a report or on an embedded report, provide pageName field in the _embedConfig_.
|Set Page (Report type only)|To set a page when embedding a report or on an embedded report, provide pageName field in the _embedConfig_.|
|Create report|To create a new report, pass the component with at least _type_, _embedUrl_ and _datasetId_ in _embedConfig_ prop.|
__Note__: To use this library in IE browser, use [react-app-polyfill](https://www.npmjs.com/package/react-app-polyfill) to add support for the incompatible features. Refer to the imports of [demo/index.tsx](https://github.com/microsoft/powerbi-client-react/blob/master/demo/index.tsx).
@ -105,7 +109,8 @@ interface EmbedProps {
| ITileEmbedConfiguration
| IQnaEmbedConfiguration
| IVisualEmbedConfiguration
| IEmbedConfiguration
| IPaginatedReportLoadConfiguration
| IReportCreateConfiguration
// Callback method to get the embedded PowerBI entity object (optional)
getEmbeddedComponent?: { (embeddedComponent: Embed): void }
@ -122,12 +127,67 @@ interface EmbedProps {
// Provide instance of PowerBI service (optional)
service?: service.Service
}
type EventHandler = {
(event?: service.ICustomEvent<any>, embeddedEntity?: Embed): void | null;
};
```
## Supported Events
### Events supported by various Power BI entities:
|Entity|Event|
|:----- |:----- |
| Report | "buttonClicked", "commandTriggered", "dataHyperlinkClicked", "dataSelected", "loaded", "pageChanged", "rendered", "saveAsTriggered", "saved", "selectionChanged", "visualClicked", "visualRendered" |
| Dashboard | "loaded", "tileClicked" |
| Tile | "tileLoaded", "tileClicked" |
| QnA | "visualRendered" |
### Event Handler to be used with Map
```ts
type EventHandler = (event?: service.ICustomEvent<any>, embeddedEntity?: Embed) => void | null;
```
## Using supported SDK methods for Power BI artifacts
### Import
*Import the 'PowerBIEmbed' inside your targeted component file:*
```ts
import { PowerBIEmbed } from 'powerbi-client-react';
```
### Use
You can use ```report``` state to call supported SDK APIs.
Steps:
1. Create one state for storing the report object, for example, ```const [report, setReport] = useState<Report>();```.
2. Use the ```setReport``` method inside the component to set the report object.
<br />
```ts
<PowerBIEmbed
embedConfig = { sampleReportConfig }
eventHandlers = { eventHandlersMap }
cssClassName = { reportClass }
getEmbeddedComponent = { (embedObject: Embed) => {
setReport(embedObject as Report);
} }
/>
```
3. Once the report object is set, it can be used to call SDK methods such as ```getVisuals```, ```getBookmarks```, etc.
<br />
```ts
async getReportPages(): Page[] {
// this.report is a class variable, initialized in step 3
const activePage: Page | undefined = await report.getActivePage();
console.log(pages);
}
```
### Dependencies
[powerbi-client](https://www.npmjs.com/package/powerbi-client)
@ -136,6 +196,10 @@ type EventHandler = {
[react](https://www.npmjs.com/package/react)
### Trademarks
This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft trademarks or logos is subject to and must follow [Microsofts Trademark & Brand Guidelines](https://www.microsoft.com/legal/intellectualproperty/trademarks/usage/general). Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. Any use of third-party trademarks or logos are subject to those third-partys policies.
### Contributing
This project welcomes contributions and suggestions. Most contributions require you to agree to a
@ -146,6 +210,14 @@ When you submit a pull request, a CLA bot will automatically determine whether y
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.
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.
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
### Data Collection.
The software may collect information about you and your use of the software and send it to Microsoft. Microsoft may use this information to provide services and improve our products and services. You may turn off the telemetry as described in the repository. There are also some features in the software that may enable you and Microsoft to collect data from users of your applications.
If you use these features, you must comply with applicable law, including providing appropriate notices to users of your applications together with a copy of Microsofts privacy statement.
Our privacy statement is located at [Microsoft Privacy Statement](https://privacy.microsoft.com/privacystatement). You can learn more about data collection and use in the help documentation and our privacy statement. Your use of the software operates as your consent to these practices.
### Support
Our public support page is available at [Microsoft Support Statement](https://powerbi.microsoft.com/support/).

16
React/.editorconfig Normal file
Просмотреть файл

@ -0,0 +1,16 @@
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.ts]
quote_type = single
[*.md]
max_line_length = off
trim_trailing_whitespace = false

4
React/.npmrc Normal file
Просмотреть файл

@ -0,0 +1,4 @@
# Auto generated file from Gardener Plugin CentralFeedServiceAdoptionPlugin
registry=https://pkgs.dev.azure.com/powerbi/embedded/_packaging/embedded_PublicPackages/npm/registry/
always-auth=true

14779
React/NOTICE.txt Normal file

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -3,11 +3,11 @@
"version": "1.0.0",
"description": "Demo for usage of powerbi-client-react",
"scripts": {
"demo": "webpack-dev-server --static ./ --open"
"demo": "webpack-dev-server --static ./src/ --open"
},
"license": "MIT",
"dependencies": {
"powerbi-client-react": "^1.3.5",
"powerbi-client-react": "^1.4.0",
"powerbi-report-authoring": "^1.1",
"react-app-polyfill": "^1.0.6"
},
@ -21,10 +21,10 @@
"react": "^16.13.1",
"react-dom": "^16.13.1",
"style-loader": "^1.2.1",
"ts-loader": "^7.0.5",
"typescript": "^3.9.3",
"ts-loader": "^9.4.2",
"typescript": "^4.8.4",
"webpack": "^5.71.0",
"webpack-cli": "^4.9.2",
"webpack-dev-server": "^4.8.1"
"webpack-dev-server": "^4.11.1"
}
}

Просмотреть файл

@ -0,0 +1,98 @@
/* Copyright (c) Microsoft Corporation.
Licensed under the MIT License. */
.report-container {
height: 75vh;
margin: 8px auto;
width: 90%;
}
body {
font-family: 'Segoe UI';
margin: 0;
}
.container {
display: flex;
flex-direction: column;
height: 100vh;
}
.header {
background: #3476ae 0 0 no-repeat padding-box;
border: 1px solid #707070;
color: #ffffff;
font: 700 22px/27px 'Segoe UI';
padding: 13px 13px 13px 36px;
text-align: left;
}
.display-message {
align-items: center;
display: flex;
font: 400 18px/27px 'Segoe UI';
height: 30px;
justify-content: center;
margin-top: 8px;
text-align: center;
}
.position {
margin-top: 40vh;
}
.embed-report {
margin-top: 18px;
text-align: center;
margin-right: 0;
}
.controls {
margin-top: 20px;
text-align: center;
flex: 1;
}
.footer {
align-items: center;
background: #f7f8fa 0 0 no-repeat padding-box;
display: flex;
font: 400 16px/21px 'Segoe UI';
height: 42px;
justify-content: center;
width: 100%;
}
.footer * {
padding: 0 3px;
}
.footer-icon {
border-radius: 50%;
height: 22px;
vertical-align: middle;
}
.footer a {
color: #3a3a3a;
text-decoration: underline;
}
button {
background: #337ab7;
border: 0;
border-radius: 5px;
color: #ffffff;
font-size: 16px;
height: 35px;
margin-right: 15px;
width: 160px;
}
button:hover {
cursor: pointer;
}
iframe {
border: none;
}

262
React/demo/src/DemoApp.tsx Normal file
Просмотреть файл

@ -0,0 +1,262 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import React, { useState, useEffect } from 'react';
import { models, Report, Embed, service, Page } from 'powerbi-client';
import { IHttpPostMessageResponse } from 'http-post-message';
import { PowerBIEmbed } from 'powerbi-client-react';
import 'powerbi-report-authoring';
import { sampleReportUrl } from './public/constants';
import './DemoApp.css';
// Root Component to demonstrate usage of embedded component
function DemoApp (): JSX.Element {
// PowerBI Report object (to be received via callback)
const [report, setReport] = useState<Report>();
// Track Report embedding status
const [isEmbedded, setIsEmbedded] = useState<boolean>(false);
// Overall status message of embedding
const [displayMessage, setMessage] = useState(`The report is bootstrapped. Click the Embed Report button to set the access token`);
// CSS Class to be passed to the embedded component
const reportClass = 'report-container';
// Pass the basic embed configurations to the embedded component to bootstrap the report on first load
// Values for properties like embedUrl, accessToken and settings will be set on click of button
const [sampleReportConfig, setReportConfig] = useState<models.IReportEmbedConfiguration>({
type: 'report',
embedUrl: undefined,
tokenType: models.TokenType.Embed,
accessToken: undefined,
settings: undefined,
});
/**
* Map of event handlers to be applied to the embedded report
* Update event handlers for the report by redefining the map using the setEventHandlersMap function
* Set event handler to null if event needs to be removed
* More events can be provided from here
* https://docs.microsoft.com/en-us/javascript/api/overview/powerbi/handle-events#report-events
*/
const[eventHandlersMap, setEventHandlersMap] = useState<Map<string, (event?: service.ICustomEvent<any>, embeddedEntity?: Embed) => void | null>>(new Map([
['loaded', () => console.log('Report has loaded')],
['rendered', () => console.log('Report has rendered')],
['error', (event?: service.ICustomEvent<any>) => {
if (event) {
console.error(event.detail);
}
},
],
['visualClicked', () => console.log('visual clicked')],
['pageChanged', (event) => console.log(event)],
]));
useEffect(() => {
if (report) {
report.setComponentTitle('Embedded Report');
}
}, [report]);
/**
* Embeds report
*
* @returns Promise<void>
*/
const embedReport = async (): Promise<void> => {
console.log('Embed Report clicked');
// Get the embed config from the service
const reportConfigResponse = await fetch(sampleReportUrl);
if (reportConfigResponse === null) {
return;
}
if (!reportConfigResponse?.ok) {
console.error(`Failed to fetch config for report. Status: ${ reportConfigResponse.status } ${ reportConfigResponse.statusText }`);
return;
}
const reportConfig = await reportConfigResponse.json();
// Update the reportConfig to embed the PowerBI report
setReportConfig({
...sampleReportConfig,
embedUrl: reportConfig.EmbedUrl,
accessToken: reportConfig.EmbedToken.Token
});
setIsEmbedded(true);
// Update the display message
setMessage('Use the buttons above to interact with the report using Power BI Client APIs.');
};
/**
* Hide Filter Pane
*
* @returns Promise<IHttpPostMessageResponse<void> | undefined>
*/
const hideFilterPane = async (): Promise<IHttpPostMessageResponse<void> | undefined> => {
// Check if report is available or not
if (!report) {
setDisplayMessageAndConsole('Report not available');
return;
}
// New settings to hide filter pane
const settings = {
panes: {
filters: {
expanded: false,
visible: false,
},
},
};
try {
const response: IHttpPostMessageResponse<void> = await report.updateSettings(settings);
// Update display message
setDisplayMessageAndConsole('Filter pane is hidden.');
return response;
} catch (error) {
console.error(error);
return;
}
};
/**
* Set data selected event
*
* @returns void
*/
const setDataSelectedEvent = () => {
setEventHandlersMap(new Map<string, (event?: service.ICustomEvent<any>, embeddedEntity?: Embed) => void | null> ([
...eventHandlersMap,
['dataSelected', (event) => console.log(event)],
]));
setMessage('Data Selected event set successfully. Select data to see event in console.');
}
/**
* Change visual type
*
* @returns Promise<void>
*/
const changeVisualType = async (): Promise<void> => {
// Check if report is available or not
if (!report) {
setDisplayMessageAndConsole('Report not available');
return;
}
// Get active page of the report
const activePage: Page | undefined = await report.getActivePage();
if (!activePage) {
setMessage('No Active page found');
return;
}
try {
// Change the visual type using powerbi-report-authoring
// For more information: https://docs.microsoft.com/en-us/javascript/api/overview/powerbi/report-authoring-overview
const visual = await activePage.getVisualByName('VisualContainer6');
const response = await visual.changeType('lineChart');
setDisplayMessageAndConsole(`The ${visual.type} was updated to lineChart.`);
return response;
}
catch (error) {
if (error === 'PowerBIEntityNotFound') {
console.log('No Visual found with that name');
} else {
console.log(error);
}
}
};
/**
* Set display message and log it in the console
*
* @returns void
*/
const setDisplayMessageAndConsole = (message: string): void => {
setMessage(message);
console.log(message);
}
const controlButtons =
isEmbedded ?
<>
<button onClick = { changeVisualType }>
Change visual type</button>
<button onClick = { hideFilterPane }>
Hide filter pane</button>
<button onClick = { setDataSelectedEvent }>
Set event</button>
<label className = "display-message">
{ displayMessage }
</label>
</>
:
<>
<label className = "display-message position">
{ displayMessage }
</label>
<button onClick = { embedReport } className = "embed-report">
Embed Report</button>
</>;
const header =
<div className = "header">Power BI Embedded React Component Demo</div>;
const reportComponent =
<PowerBIEmbed
embedConfig = { sampleReportConfig }
eventHandlers = { eventHandlersMap }
cssClassName = { reportClass }
getEmbeddedComponent = { (embedObject: Embed) => {
console.log(`Embedded object of type "${ embedObject.embedtype }" received`);
setReport(embedObject as Report);
} }
/>;
const footer =
<div className = "footer">
<p>This demo is powered by Power BI Embedded Analytics</p>
<label className = "separator-pipe">|</label>
<img title = "Power-BI" alt = "PowerBI_Icon" className = "footer-icon" src = "./assets/PowerBI_Icon.png" />
<p>Explore our<a href = "https://aka.ms/pbijs/" target = "_blank" rel = "noreferrer noopener">Playground</a></p>
<label className = "separator-pipe">|</label>
<img title = "GitHub" alt = "GitHub_Icon" className = "footer-icon" src = "./assets/GitHub_Icon.png" />
<p>Find the<a href = "https://github.com/microsoft/PowerBI-client-react" target = "_blank" rel = "noreferrer noopener">source code</a></p>
</div>;
return (
<div className = "container">
{ header }
<div className = "controls">
{ controlButtons }
{ isEmbedded ? reportComponent : null }
</div>
{ footer }
</div>
);
}
export default DemoApp;

Двоичные данные
React/demo/src/assets/GitHub_Icon.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 7.1 KiB

Двоичные данные
React/demo/src/assets/PowerBI_Icon.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 2.0 KiB

16
React/demo/src/index.html Normal file
Просмотреть файл

@ -0,0 +1,16 @@
<!-- Copyright (c) Microsoft Corporation.
Licensed under the MIT License. -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="./public/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>React Wrapper demo</title>
</head>
<body>
<div id="root"></div>
<script src="./bundle.js"></script>
</body>
</html>

Просмотреть файл

@ -1,13 +1,13 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import 'react-app-polyfill/ie11'; // For IE compatibility
import 'react-app-polyfill/stable'; // For IE compatibility
import React from 'react';
import ReactDOM from 'react-dom';
import DemoApp from './DemoApp';
ReactDOM.render(
<DemoApp/>,
document.getElementById('root')
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import 'react-app-polyfill/ie11'; // For IE compatibility
import 'react-app-polyfill/stable'; // For IE compatibility
import React from 'react';
import ReactDOM from 'react-dom';
import DemoApp from './DemoApp';
ReactDOM.render(
<DemoApp/>,
document.getElementById('root')
);

Просмотреть файл

@ -0,0 +1,5 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
// Endpoint to get report config
export const sampleReportUrl = 'https://aka.ms/CaptureViewsReportEmbedConfig';

Двоичные данные
React/demo/src/public/favicon.ico Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 3.8 KiB

Просмотреть файл

@ -1,18 +1,18 @@
{
"include": [
"./**/*.tsx",
"./**/*.ts"
],
"compilerOptions": {
"target": "es5",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"noErrorTruncation": true,
"forceConsistentCasingInFileNames": true,
"module": "ES6",
"moduleResolution": "node",
"resolveJsonModule": true,
"jsx": "react",
}
{
"include": [
"./**/*.tsx",
"./**/*.ts"
],
"compilerOptions": {
"target": "es6",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"noErrorTruncation": true,
"forceConsistentCasingInFileNames": true,
"module": "ES6",
"moduleResolution": "node",
"resolveJsonModule": true,
"jsx": "react",
}
}

Просмотреть файл

@ -1,36 +1,36 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
const 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',
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
const path = require('path');
module.exports = {
mode: 'development',
entry: path.resolve('src/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,4 @@
# Auto generated file from Gardener Plugin CentralFeedServiceAdoptionPlugin
registry=https://pkgs.dev.azure.com/powerbi/embedded/_packaging/embedded_PublicPackages/npm/registry/
always-auth=true

Просмотреть файл

@ -1,21 +1,21 @@
{
"include": [
"../../src/**/*.tsx",
"../../src/**/*.ts"
],
"compilerOptions": {
"lib": ["ES2016"],
"target": "es5",
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true,
"noErrorTruncation": true,
"module": "ES6",
"moduleResolution": "node",
"jsx": "react",
"sourceMap": true,
"noImplicitAny": true,
"declaration": true,
"outDir": "../../dist",
}
{
"include": [
"../../src/**/*.tsx",
"../../src/**/*.ts"
],
"compilerOptions": {
"lib": ["ES2016"],
"target": "es5",
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true,
"noErrorTruncation": true,
"module": "ES6",
"moduleResolution": "node",
"jsx": "react",
"sourceMap": true,
"noImplicitAny": true,
"declaration": true,
"outDir": "../../dist",
}
}

Просмотреть файл

@ -1,40 +1,40 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
let path = require('path');
module.exports = {
entry: path.resolve('src/PowerBIEmbed.tsx'),
output: {
library: 'powerbi-client-react',
libraryTarget: 'umd',
path: path.resolve('dist'),
filename: 'powerbi-client-react.js'
},
externals: [
'react',
'powerbi-client',
'lodash.isequal'
],
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',
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
const path = require('path');
module.exports = {
entry: path.resolve('src/PowerBIEmbed.tsx'),
output: {
library: 'powerbi-client-react',
libraryTarget: 'umd',
path: path.resolve('dist'),
filename: 'powerbi-client-react.js'
},
externals: [
'react',
'powerbi-client',
'lodash.isequal'
],
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',
};

Просмотреть файл

@ -1,70 +1,70 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
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,
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
],
// start these browsers
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
browsers: ["Chrome_headless"],
customLaunchers: {
'Chrome_headless': {
base: 'Chrome',
flags: [
'--no-sandbox',
]
},
},
// 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
})
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
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,
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
],
// start these browsers
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
browsers: ["Chrome_headless"],
customLaunchers: {
'Chrome_headless': {
base: 'Chrome',
flags: [
'--no-sandbox',
]
},
},
// 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
})
}

Просмотреть файл

@ -1,20 +1,20 @@
{
"include": [
"../../test/**/*.tsx",
"../../test/**/*.ts"
],
"compilerOptions": {
"target": "ES5",
"lib": [
"ES2016",
"dom"
],
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": false,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"jsx": "react"
}
{
"include": [
"../../test/**/*.tsx",
"../../test/**/*.ts"
],
"compilerOptions": {
"target": "ES5",
"lib": [
"ES2016",
"dom"
],
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": false,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"jsx": "react"
}
}

Просмотреть файл

@ -1,36 +1,36 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
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'
]
},
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
const 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'
]
},
};

Просмотреть файл

@ -1,6 +1,6 @@
{
"name": "powerbi-client-react",
"version": "1.3.5",
"version": "1.4.0",
"description": "React wrapper for powerbi-client library",
"main": "dist/powerbi-client-react.js",
"types": "dist/powerbi-client-react.d.ts",
@ -13,8 +13,7 @@
"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",
"demo": "cd ../demo && npm install && npm run demo",
"lint": "eslint --fix src/**/*.{ts,tsx}"
},
"keywords": [
@ -41,8 +40,8 @@
"@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",
"@typescript-eslint/eslint-plugin": "^5.42.0",
"@typescript-eslint/parser": "^5.42.0",
"eslint": "^7.4.0",
"eslint-plugin-react": "^7.20.0",
"jasmine-core": "^3.5.0",
@ -52,8 +51,8 @@
"react": "^16.13.1",
"react-app-polyfill": "^1.0.6",
"react-dom": "^16.13.1",
"ts-loader": "^7.0.5",
"typescript": "^3.9.6",
"ts-loader": "^9.4.1",
"typescript": "^4.8.4",
"webpack": "^5.71.0",
"webpack-cli": "^4.9.2"
}

Просмотреть файл

@ -1,386 +1,399 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import * as React from 'react';
import {
service,
factories,
Report,
Embed,
Dashboard,
Tile,
Qna,
Visual,
IEmbedSettings,
IEmbedConfiguration,
IQnaEmbedConfiguration,
IVisualEmbedConfiguration,
IReportEmbedConfiguration,
IDashboardEmbedConfiguration,
ITileEmbedConfiguration,
} from 'powerbi-client';
import { ReportLevelFilters, FiltersOperations } from 'powerbi-models';
import isEqual from 'lodash.isequal';
import { stringifyMap, SdkType, SdkWrapperVersion } from './utils';
/**
* Type for event handler function of embedded entity
*/
export type EventHandler = {
(event?: service.ICustomEvent<any>, embeddedEntity?: Embed): void | null;
};
/**
* Props interface for PowerBIEmbed component
*/
export interface EmbedProps {
// Configuration for embedding the PowerBI entity (Required)
embedConfig:
| IReportEmbedConfiguration
| IDashboardEmbedConfiguration
| ITileEmbedConfiguration
| IQnaEmbedConfiguration
| IVisualEmbedConfiguration
| IEmbedConfiguration;
// 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, EventHandler>;
// CSS class to be set on the embedding container (Optional)
cssClassName?: string;
// Phased embedding flag (Optional)
phasedEmbedding?: boolean;
// 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
// Note: Do not read or assign to this member variable directly, instead use the getter and setter
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 = '';
// Getter for this._embed
private get embed(): Embed | undefined {
return this._embed;
};
// Setter for this._embed
private set embed(newEmbedInstance: Embed | undefined) {
this._embed = newEmbedInstance;
// Invoke callback method in props to return this embed instance
this.invokeGetEmbedCallback();
};
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);
}
this.powerbi.setSdkInfo(SdkType, SdkWrapperVersion);
};
componentDidMount(): void {
// Check if HTML container is available
if (this.containerRef.current) {
// Decide to embed, load or bootstrap
if (this.props.embedConfig.accessToken && this.props.embedConfig.embedUrl) {
this.embedEntity();
}
else {
this.embed = this.powerbi.bootstrap(this.containerRef.current, this.props.embedConfig);
}
}
// Set event handlers if available
if (this.props.eventHandlers && this.embed) {
this.setEventHandlers(this.embed, this.props.eventHandlers);
}
};
async componentDidUpdate(prevProps: EmbedProps): Promise<void> {
this.embedOrUpdateAccessToken(prevProps);
// Set event handlers if available
if (this.props.eventHandlers && this.embed) {
this.setEventHandlers(this.embed, this.props.eventHandlers);
}
// Allow settings update only when settings object in embedConfig of current and previous props is different
if (!isEqual(this.props.embedConfig.settings, prevProps.embedConfig.settings)) {
await this.updateSettings();
}
// Update pageName and filters for a report
if (this.props.embedConfig.type === EmbedType.Report) {
try {
// Typecasting to IReportEmbedConfiguration
const embedConfig = this.props.embedConfig as IReportEmbedConfiguration;
const filters = embedConfig.filters as ReportLevelFilters[];
const prevEmbedConfig = prevProps.embedConfig as IReportEmbedConfiguration;
// Set new page if available and different from the previous page
if (embedConfig.pageName && embedConfig.pageName !== prevEmbedConfig.pageName) {
// Upcast to Report and call setPage
await (this.embed as Report).setPage(embedConfig.pageName);
}
// Set filters on the embedded report if available and different from the previous filter
if (filters && !isEqual(filters, prevEmbedConfig.filters)) {
// Upcast to Report and call updateFilters with the Replace filter operation
await (this.embed as Report).updateFilters(FiltersOperations.Replace, filters);
}
// Remove filters on the embedded report, if previously applied
else if (!filters && prevEmbedConfig.filters) {
// Upcast to Report and call updateFilters with the RemoveAll filter operation
await (this.embed as Report).updateFilters(FiltersOperations.RemoveAll);
}
} catch (err) {
console.error(err);
}
}
};
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>
)
};
/**
* Embed the powerbi entity (Load for phased embedding)
*/
private embedEntity(): void {
// Check if the HTML container is rendered and available
if (!this.containerRef.current) {
return;
}
// Load when props.phasedEmbedding is true and embed type is report, embed otherwise
if (this.props.phasedEmbedding && this.props.embedConfig.type === EmbedType.Report) {
this.embed = this.powerbi.load(this.containerRef.current, this.props.embedConfig);
}
else {
if (this.props.phasedEmbedding) {
console.error(`Phased embedding is not supported for type ${this.props.embedConfig.type}`)
}
this.embed = this.powerbi.embed(this.containerRef.current, this.props.embedConfig);
}
}
/**
* When component updates, choose to _embed_ the powerbi entity or _update the accessToken_ in the embedded entity
* or do nothing if 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 or load 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.embedEntity();
}
// 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}`);
});
}
}
/**
* 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, EventHandler>
): 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((eventHandlerMethod, eventName) => {
// Check if this event is allowed
if (allowedEvents.includes(eventName)) {
// Removes event handler for this event
embed.off(eventName);
// Event handler is effectively removed for this event when eventHandlerMethod is null
if (eventHandlerMethod) {
// Set single event handler
embed.on(eventName, (event: service.ICustomEvent<any>): void => {
eventHandlerMethod(event, this.embed);
});
}
}
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 invokeGetEmbedCallback(): void {
if (this.props.getEmbeddedComponent && this.embed) {
this.props.getEmbeddedComponent(this.embed);
}
};
/**
* Update settings from props of the embedded artifact
*
* @returns void
*/
private async updateSettings(): Promise<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;
try {
// Upcast to Report and call updateSettings
await (this.embed as Report).updateSettings(settings);
} catch (error) {
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}`);
}
};
}
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import * as React from 'react';
import {
service,
factories,
Report,
Embed,
Dashboard,
Tile,
Qna,
Visual,
IEmbedSettings,
IQnaEmbedConfiguration,
IVisualEmbedConfiguration,
IReportEmbedConfiguration,
IDashboardEmbedConfiguration,
ITileEmbedConfiguration,
} from 'powerbi-client';
import { IReportCreateConfiguration, IPaginatedReportLoadConfiguration, ReportLevelFilters, FiltersOperations } from 'powerbi-models';
import isEqual from 'lodash.isequal';
import { stringifyMap, SdkType, SdkWrapperVersion } from './utils';
/**
* Type for event handler function of embedded entity
*/
export type EventHandler = {
(event?: service.ICustomEvent<any>, embeddedEntity?: Embed): void | null;
};
/**
* Props interface for PowerBIEmbed component
*/
export interface EmbedProps {
// Configuration for embedding the PowerBI entity (Required)
embedConfig:
| IReportEmbedConfiguration
| IDashboardEmbedConfiguration
| ITileEmbedConfiguration
| IQnaEmbedConfiguration
| IVisualEmbedConfiguration
| IPaginatedReportLoadConfiguration
| IReportCreateConfiguration;
// 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, EventHandler>;
// CSS class to be set on the embedding container (Optional)
cssClassName?: string;
// Phased embedding flag (Optional)
phasedEmbedding?: boolean;
// Provide a custom implementation of PowerBI service (Optional)
service?: service.Service;
}
export enum EmbedType {
Create = 'create',
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
// Note: Do not read or assign to this member variable directly, instead use the getter and setter
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 = '';
// Getter for this._embed
private get embed(): Embed | undefined {
return this._embed;
};
// Setter for this._embed
private set embed(newEmbedInstance: Embed | undefined) {
this._embed = newEmbedInstance;
// Invoke callback method in props to return this embed instance
this.invokeGetEmbedCallback();
};
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);
}
this.powerbi.setSdkInfo(SdkType, SdkWrapperVersion);
};
componentDidMount(): void {
// Check if HTML container is available
if (this.containerRef.current) {
// Decide to embed, load or bootstrap
if (this.props.embedConfig.accessToken && this.props.embedConfig.embedUrl) {
this.embedEntity();
}
else {
this.embed = this.powerbi.bootstrap(this.containerRef.current, this.props.embedConfig);
}
}
// Set event handlers if available
if (this.props.eventHandlers && this.embed) {
this.setEventHandlers(this.embed, this.props.eventHandlers);
}
};
async componentDidUpdate(prevProps: EmbedProps): Promise<void> {
this.embedOrUpdateAccessToken(prevProps);
// Set event handlers if available
if (this.props.eventHandlers && this.embed) {
this.setEventHandlers(this.embed, this.props.eventHandlers);
}
// Allow settings update only when settings object in embedConfig of current and previous props is different
if (!isEqual(this.props.embedConfig.settings, prevProps.embedConfig.settings)) {
await this.updateSettings();
}
// Update pageName and filters for a report
if (this.props.embedConfig.type === EmbedType.Report) {
try {
// Typecasting to IReportEmbedConfiguration
const embedConfig = this.props.embedConfig as IReportEmbedConfiguration;
const filters = embedConfig.filters as ReportLevelFilters[];
const prevEmbedConfig = prevProps.embedConfig as IReportEmbedConfiguration;
// Set new page if available and different from the previous page
if (embedConfig.pageName && embedConfig.pageName !== prevEmbedConfig.pageName) {
// Upcast to Report and call setPage
await (this.embed as Report).setPage(embedConfig.pageName);
}
// Set filters on the embedded report if available and different from the previous filter
if (filters && !isEqual(filters, prevEmbedConfig.filters)) {
// Upcast to Report and call updateFilters with the Replace filter operation
await (this.embed as Report).updateFilters(FiltersOperations.Replace, filters);
}
// Remove filters on the embedded report, if previously applied
else if (!filters && prevEmbedConfig.filters) {
// Upcast to Report and call updateFilters with the RemoveAll filter operation
await (this.embed as Report).updateFilters(FiltersOperations.RemoveAll);
}
} catch (err) {
console.error(err);
}
}
};
componentWillUnmount(): void {
// Clean Up
if (this.containerRef.current) {
this.powerbi.reset(this.containerRef.current);
}
// Set the previous event handler map string to empty
this.prevEventHandlerMapString = '';
};
render(): JSX.Element {
return (
<div
ref={this.containerRef}
className={this.props.cssClassName}>
</div>
)
};
/**
* Embed the powerbi entity (Load for phased embedding)
*/
private embedEntity(): void {
// Check if the HTML container is rendered and available
if (!this.containerRef.current) {
return;
}
// Load when props.phasedEmbedding is true and embed type is report, embed otherwise
if (this.props.phasedEmbedding && this.props.embedConfig.type === EmbedType.Report) {
this.embed = this.powerbi.load(this.containerRef.current, this.props.embedConfig);
}
else {
if (this.props.phasedEmbedding) {
console.error(`Phased embedding is not supported for type ${this.props.embedConfig.type}`)
}
if (this.props.embedConfig.type === EmbedType.Create) {
this.embed = this.powerbi.createReport(this.containerRef.current, this.props.embedConfig as IReportCreateConfiguration);
}
else {
this.embed = this.powerbi.embed(this.containerRef.current, this.props.embedConfig);
}
}
}
/**
* When component updates, choose to _embed_ the powerbi entity or _update the accessToken_ in the embedded entity
* or do nothing if the embedUrl and accessToken did not update in the new props
*
* @param prevProps EmbedProps
* @returns void
*/
private async embedOrUpdateAccessToken(prevProps: EmbedProps): Promise<void> {
// Check if Embed URL and Access Token are present in current props
if (!this.props.embedConfig.accessToken || !this.props.embedConfig.embedUrl) {
return;
}
// Embed or load 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.embedEntity();
}
// 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
) {
try {
await this.embed.setAccessToken(this.props.embedConfig.accessToken);
} catch(error) {
console.error("setAccessToken error:\n", error);
}
}
}
/**
* 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, EventHandler>
): 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.Create:
break;
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((eventHandlerMethod, eventName) => {
// Check if this event is allowed
if (allowedEvents.includes(eventName)) {
// Removes event handler for this event
embed.off(eventName);
// Event handler is effectively removed for this event when eventHandlerMethod is null
if (eventHandlerMethod) {
// Set single event handler
embed.on(eventName, (event: service.ICustomEvent<any>): void => {
eventHandlerMethod(event, this.embed);
});
}
}
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 invokeGetEmbedCallback(): void {
if (this.props.getEmbeddedComponent && this.embed) {
this.props.getEmbeddedComponent(this.embed);
}
};
/**
* Update settings from props of the embedded artifact
*
* @returns void
*/
private async updateSettings(): Promise<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;
try {
// Upcast to Report and call updateSettings
await (this.embed as Report).updateSettings(settings);
} catch (error) {
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}`);
}
};
}

Просмотреть файл

@ -1,9 +1,9 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
export {
PowerBIEmbed,
EmbedProps,
EmbedType,
EventHandler
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
export {
PowerBIEmbed,
EmbedProps,
EmbedType,
EventHandler
} from './PowerBIEmbed'

Просмотреть файл

@ -1,47 +1,47 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
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() : ''
];
}));
};
// SDK information to be used with service instance
export const SdkType = "powerbi-client-react";
export const SdkWrapperVersion = "1.3.5";
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
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() : ''
];
}));
};
// SDK information to be used with service instance
export const SdkType = "powerbi-client-react";
export const SdkWrapperVersion = "1.4.0";

Просмотреть файл

@ -7,10 +7,12 @@ import React from 'react';
import ReactDOM from 'react-dom';
import { act, isElement } from 'react-dom/test-utils';
import { Report, Dashboard, service, factories, IEmbedSettings, IReportEmbedConfiguration } from 'powerbi-client';
import { PowerBIEmbed } from '../src/PowerBIEmbed';
import { mockPowerBIService, mockedMethods } from "./mockService";
import { IBasicFilter, FilterType, FiltersOperations } from 'powerbi-models';
import { PowerBIEmbed } from '../src/PowerBIEmbed';
import { stringifyMap } from '../src/utils';
// Use this function to render powerbi entity with only config
function renderReport(container: HTMLDivElement, config) {
let testReport: Report = undefined;
@ -944,6 +946,39 @@ describe('tests of PowerBIEmbed', function () {
});
describe('tests for setting event handlers', () => {
it('test event handlers are setting when remounting twice', () => {
// Arrange
const eventHandlers = new Map([
['loaded', function () { }],
['rendered', function () { }],
['error', function () { }]
]);
const powerbi = new service.Service(
factories.hpmFactory,
factories.wpmpFactory,
factories.routerFactory);
const embed = powerbi.bootstrap(container, { type: 'report' });
// Act
const powerbiembed = new PowerBIEmbed({
embedConfig: { type: 'report' },
eventHandlers: eventHandlers
});
// Ignoring next line as setEventHandlers is a private method
// @ts-ignore
powerbiembed.setEventHandlers(embed, eventHandlers);
powerbiembed.componentWillUnmount();
expect((powerbiembed as any).prevEventHandlerMapString).toBe('');
powerbiembed.componentDidMount();
// @ts-ignore
powerbiembed.setEventHandlers(embed, eventHandlers);
// Assert
expect((powerbiembed as any).prevEventHandlerMapString).toBe(stringifyMap(eventHandlers));
});
it('clears and sets the event handlers', () => {
// Arrange
@ -1138,4 +1173,4 @@ describe('tests of PowerBIEmbed', function () {
expect(testReport.on).not.toHaveBeenCalled();
});
});
});
});

Просмотреть файл

@ -1,8 +1,8 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
const mockedMethods = ['init', 'embed', 'bootstrap', 'load', 'get', 'reset', 'preload', 'setSdkInfo'];
const mockPowerBIService = jasmine.createSpyObj('mockService', mockedMethods);
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
const mockedMethods = ['init', 'embed', 'bootstrap', 'load', 'get', 'reset', 'preload', 'setSdkInfo'];
const mockPowerBIService = jasmine.createSpyObj('mockService', mockedMethods);
export { mockPowerBIService, mockedMethods };

Просмотреть файл

@ -1,71 +1,71 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
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);
});
});
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
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);
});
});
});

Просмотреть файл

@ -14,7 +14,7 @@ Instead, please report them to the Microsoft Security Response Center (MSRC) at
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).
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).
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).
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:

Просмотреть файл

@ -1,93 +0,0 @@
.report-style-class {
height: 69vh;
margin: 1% auto;
width: 60%;
}
body {
font-family: 'Segoe UI';
margin: 0;
}
.header {
background: #3476AE 0 0 no-repeat padding-box;
border: 1px solid #707070;
height: 55px;
left: 0;
top: 0;
}
.displayMessage {
color: #000000;
font: normal 22px/27px Segoe UI;
letter-spacing: 0;
margin-top: 1%;
opacity: 1;
text-align: center;
}
.hr {
border: 1px solid #E0E0E0;
opacity: 1;
}
.controls {
margin-top: 1%;
text-align: center;
}
.footer {
background: #EEF3F8 0 0 no-repeat padding-box;
bottom: 0;
height: 39px;
opacity: 1;
position: absolute;
width: 100%;
}
.footer-text {
font: Regular 16px/21px Segoe UI;
height: 21px;
letter-spacing: 0;
margin-top: 9px;
opacity: 1;
position: relative;
text-align: center;
width: 100%;
}
.footer-text > a {
color: #278CE2;
font: Regular 16px/21px Segoe UI;
letter-spacing: 0;
text-decoration: underline;
}
.title {
color: #FFFFFF;
font: Bold 22px/27px Segoe UI;
letter-spacing: 0;
margin: 13px;
margin-left: 36px;
opacity: 1;
text-align: left;
}
button {
background: #337AB7;
border: 0;
border-radius: 5px;
color: #FFFFFF;
font-size: medium;
height: 35px;
margin-right: 15px;
width: 150px;
}
button:onfocus {
outline: none;
}
iframe {
border: none;
}

Просмотреть файл

@ -1,199 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import React, { useState } from 'react';
import { models, Report, Embed, service, Page } from 'powerbi-client';
import { PowerBIEmbed } from 'powerbi-client-react';
import 'powerbi-report-authoring';
import './DemoApp.css';
// Root Component to demonstrate usage of wrapper component
function DemoApp (): JSX.Element {
// PowerBI Report object (to be received via callback)
const [report, setReport] = useState<Report>();
// API end-point url to get embed config for a sample report
const sampleReportUrl = 'https://playgroundbe-bck-1.azurewebsites.net/Reports/SampleReport';
// Report config useState hook
// Values for properties like embedUrl, accessToken and settings will be set on click of buttons below
const [sampleReportConfig, setReportConfig] = useState<models.IReportEmbedConfiguration>({
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('The report is rendered')
}],
['error', function (event?: service.ICustomEvent<any>) {
if (event) {
console.error(event.detail);
}
}]
]);
// Fetch sample report's config (eg. embedUrl and AccessToken) for embedding
const mockSignIn = async () => {
// Fetch sample report's embed config
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('The access token is successfully set. Loading the Power BI report')
// Set the fetched embedUrl and embedToken in the report config
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
}
}
}
});
};
// Delete the first visual using powerbi-report-authoring library
const deleteVisual = async () => {
if (!report) {
console.log('Report not available');
return;
}
const activePage = await getActivePage(report);
if (!activePage) {
console.log('No active page');
return;
}
// Get all visuals in the active page
const visuals = await activePage.getVisuals();
if (visuals.length === 0) {
console.log('No visual left');
return;
}
// Get first visible visual
const visual = visuals.find((v) => {
return v.layout.displayState?.mode === models.VisualContainerDisplayMode.Visible;
});
// No visible visual found
if (!visual) {
console.log('No visible visual available to delete');
return;
}
try {
// Documentation link: https://github.com/microsoft/powerbi-report-authoring/wiki/Visualization
// Delete the visual
await activePage.deleteVisual(visual.name);
console.log('Visual was deleted');
}
catch (error) {
console.error(error);
}
};
async function getActivePage(powerbiReport: Report): Promise<Page | undefined> {
const pages = await powerbiReport.getPages();
// Get the active page
const activePage = pages.filter(function (page) {
return page.isActive
})[0];
return activePage;
}
const [displayMessage, setMessage] = useState(`The report is bootstrapped. Click the Embed Report button to set the access token`);
const controlButtons =
<div className = "controls">
<button onClick = { mockSignIn }>
Embed Report</button>
<button onClick = { changeSettings }>
Hide filter pane</button>
<button onClick = { deleteVisual }>
Delete a Visual</button>
</div>;
const header =
<div className = "header">
<div className = "title">Power BI React component demo</div>
</div>;
const footer =
<div className = "footer">
<div className = "footer-text">
GitHub: &nbsp;
<a href="https://github.com/microsoft/PowerBI-client-react">https://github.com/microsoft/PowerBI-client-react</a>
</div>
</div>;
return (
<div>
{ header }
<PowerBIEmbed
embedConfig = { sampleReportConfig }
eventHandlers = { eventHandlersMap }
cssClassName = { "report-style-class" }
getEmbeddedComponent = { (embedObject:Embed) => {
console.log(`Embedded object of type "${ embedObject.embedtype }" received`);
setReport(embedObject as Report);
} }
/>
<div className = "hr"></div>
<div className = "displayMessage">
{ displayMessage }
</div>
{ controlButtons }
{ footer }
</div>
);
}
export default DemoApp;

Просмотреть файл

@ -1,10 +0,0 @@
<!-- Copyright (c) Microsoft Corporation.
Licensed under the MIT License. -->
<!DOCTYPE html>
<html>
<body>
<div id="root"></div>
<script src="./bundle.js"></script>
</body>
</html>

1
owners.txt Normal file
Просмотреть файл

@ -0,0 +1 @@
corembed