forked from microsoft/PowerBI-JavaScript
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathservice.ts
More file actions
292 lines (243 loc) · 10.8 KB
/
Copy pathservice.ts
File metadata and controls
292 lines (243 loc) · 10.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
import * as embed from './embed';
import { Report } from './report';
import { Tile } from './tile';
import * as utils from './util';
import * as wpmp from 'window-post-message-proxy';
import * as hpm from 'http-post-message';
import * as router from 'powerbi-router';
import * as models from 'powerbi-models';
export interface IEvent<T> {
type: string;
id: string;
name: string;
value: T;
}
export interface IEventHandler<T> {
(event: IEvent<T>): any;
}
export interface IHpmFactory {
(wpmp: wpmp.WindowPostMessageProxy, targetWindow?: Window, version?: string, type?: string, origin?: string): hpm.HttpPostMessage;
}
export interface IWpmpFactory {
(name?: string, logMessages?: boolean, eventSourceOverrideWindow?: Window): wpmp.WindowPostMessageProxy;
}
export interface IRouterFactory {
(wpmp: wpmp.WindowPostMessageProxy): router.Router;
}
export interface IPowerBiElement extends HTMLElement {
powerBiEmbed: embed.Embed;
}
export interface IDebugOptions {
logMessages?: boolean;
wpmpName?: string;
}
export interface IServiceConfiguration extends IDebugOptions {
autoEmbedOnContentLoaded?: boolean;
onError?: (error: any) => any;
version?: string;
type?: string;
}
export class Service {
/**
* List of components this service can embed.
*/
private static components: (typeof Report | typeof Tile)[] = [
Tile,
Report
];
/**
* Default configuration for service.
*/
private static defaultConfig: IServiceConfiguration = {
autoEmbedOnContentLoaded: false,
onError: (...args) => console.log(args[0], args.slice(1))
};
/** Save access token as fallback/global token to use when local token for report/tile is not provided. */
accessToken: string;
/** Configuration object */
private config: IServiceConfiguration;
/** List of components (Reports/Tiles) that have been embedded using this service instance. */
private embeds: embed.Embed[];
/** TODO: Look for way to make this private without sacraficing ease of maitenance. This should be private but in embed needs to call methods. */
public hpm: hpm.HttpPostMessage;
public wpmp: wpmp.WindowPostMessageProxy;
private router: router.Router;
constructor(hpmFactory: IHpmFactory, wpmpFactory: IWpmpFactory, routerFactory: IRouterFactory, config: IServiceConfiguration = {}) {
this.wpmp = wpmpFactory(config.wpmpName, config.logMessages);
this.hpm = hpmFactory(this.wpmp, null, config.version, config.type);
this.router = routerFactory(this.wpmp);
/**
* Add handler for report events
*/
this.router.post(`/reports/:uniqueId/events/:eventName`, (req, res) => {
const event: IEvent<any> = {
type: 'report',
id: req.params.uniqueId,
name: req.params.eventName,
value: req.body
};
this.handleEvent(event);
});
this.router.post(`/reports/:uniqueId/pages/:pageName/events/:eventName`, (req, res) => {
const event: IEvent<any> = {
type: 'report',
id: req.params.uniqueId,
name: req.params.eventName,
value: req.body
};
this.handleEvent(event);
});
this.router.post(`/reports/:uniqueId/visuals/:pageName/events/:eventName`, (req, res) => {
const event: IEvent<any> = {
type: 'report',
id: req.params.uniqueId,
name: req.params.eventName,
value: req.body
};
this.handleEvent(event);
});
this.embeds = [];
// TODO: Change when Object.assign is available.
this.config = utils.assign({}, Service.defaultConfig, config);
if (this.config.autoEmbedOnContentLoaded) {
this.enableAutoEmbed();
}
}
/**
* Handler for DOMContentLoaded which searches DOM for elements having 'powerbi-embed-url' attribute
* and automatically attempts to embed a powerbi component based on information from the attributes.
* Only runs if `config.autoEmbedOnContentLoaded` is true when the service is created.
*/
init(container?: HTMLElement, config: embed.IEmbedConfiguration = undefined): embed.Embed[] {
container = (container && container instanceof HTMLElement) ? container : document.body;
const elements = Array.prototype.slice.call(container.querySelectorAll(`[${embed.Embed.embedUrlAttribute}]`));
return elements.map(element => this.embed(element, config));
}
/**
* Given an html element embed component based on configuration.
* If component has already been created and attached to element re-use component instance and existing iframe,
* otherwise create a new component instance
*/
embed(element: HTMLElement, config: embed.IEmbedConfiguration = {}): embed.Embed {
let component: embed.Embed;
let powerBiElement = <IPowerBiElement>element;
if (powerBiElement.powerBiEmbed) {
component = this.embedExisting(powerBiElement, config);
}
else {
component = this.embedNew(powerBiElement, config);
}
return component;
}
/**
* Given an html element embed component base configuration.
* Save component instance on element for later lookup.
*/
private embedNew(element: IPowerBiElement, config: embed.IEmbedConfiguration): embed.Embed {
const componentType = config.type || element.getAttribute(embed.Embed.typeAttribute);
if (!componentType) {
throw new Error(`Attempted to embed using config ${JSON.stringify(config)} on element ${element.outerHTML}, but could not determine what type of component to embed. You must specify a type in the configuration or as an attribute such as '${embed.Embed.typeAttribute}="${Report.type.toLowerCase()}"'.`);
}
// Save type on configuration so it can be referenced later at known location
config.type = componentType;
const Component = utils.find(component => componentType === component.type.toLowerCase(), Service.components);
if (!Component) {
throw new Error(`Attempted to embed component of type: ${componentType} but did not find any matching component. Please verify the type you specified is intended.`);
}
const component = new Component(this, element, config);
element.powerBiEmbed = component;
this.embeds.push(component);
return component;
}
/**
* Given and element which arleady contains embed, load with new configuration
*/
private embedExisting(element: IPowerBiElement, config: embed.IEmbedConfiguration): embed.Embed {
const component = utils.find(x => x.element === element, this.embeds);
if (!component) {
throw new Error(`Attempted to embed using config ${JSON.stringify(config)} on element ${element.outerHTML} which already has embedded comopnent associated, but could not find the existing comopnent in the list of active components. This could indicate the embeds list is out of sync with the DOM, or the component is referencing the incorrect HTML element.`);
}
component.load(<embed.IInternalEmbedConfiguration>config);
return component;
}
/**
* Adds event handler for DOMContentLoaded which finds all elements in DOM with attribute powerbi-embed-url
* then attempts to initiate the embed process based on data from other powerbi-* attributes.
* (This is usually only useful for applications rendered on by the server since all the data needed will be available by the time the handler is called.)
*/
enableAutoEmbed() {
window.addEventListener('DOMContentLoaded', (event: Event) => this.init(document.body), false);
}
/**
* Returns instance of component associated with element.
*/
get(element: HTMLElement) {
const powerBiElement = <IPowerBiElement>element;
if (!powerBiElement.powerBiEmbed) {
throw new Error(`You attempted to get an instance of powerbi component associated with element: ${element.outerHTML} but there was no associated instance.`);
}
return powerBiElement.powerBiEmbed;
}
/**
* Find embed instance by name / unique id provided.
*/
find(uniqueId: string): Report | Tile {
return utils.find(x => x.config.uniqueId === uniqueId, this.embeds);
}
/**
* Given an html element which has component embedded within it, remove the component from list of embeds, remove association with component, and remove the iframe.
*/
reset(element: HTMLElement) {
const powerBiElement = <IPowerBiElement>element;
if (!powerBiElement.powerBiEmbed) {
return;
}
/** Remove component from internal list */
utils.remove(x => x === powerBiElement.powerBiEmbed, this.embeds);
/** Delete property from html element */
delete powerBiElement.powerBiEmbed;
/** Remove iframe from element */
const iframe = element.querySelector('iframe');
if(iframe) {
iframe.remove();
}
}
/**
* Given an event object, find embed with matching type and id and invoke its handleEvent method with event.
*/
handleEvent(event: IEvent<any>): void {
const embed = utils.find(embed => {
return (embed.config.type === event.type
&& embed.config.uniqueId === event.id);
}, this.embeds);
if(embed) {
utils.raiseCustomEvent(embed.element, event.name, event.value);
}
}
/**
* Translate target into url
* Target may be to the whole report, speific page, or specific visual
*/
private getTargetUrl(target?: models.IPageTarget | models.IVisualTarget): string {
let targetUrl;
/**
* TODO: I mentioned this issue in the protocol test, but we're tranlating targets from objects
* into parts of the url, and then back to objects. It is a trade off between complixity in this code vs semantic URIs
*
* We could come up with a different idea which passed the target as part of the body
*/
if(!target) {
targetUrl = '/report';
}
else if(target.type === "page") {
targetUrl = `/report/pages/${(<models.IPageTarget>target).name}`;
}
else if(target.type === "visual") {
targetUrl = `/report/visuals/${(<models.IVisualTarget>target).id}`;
}
else {
throw new Error(`target.type must be either 'page' or 'visual'. You passed: ${target.type}`);
}
return targetUrl;
}
}