Merged PR 3562: Visual level filters

Visual level filters update
This commit is contained in:
Amit Shuster 2017-12-24 13:38:06 +00:00
Родитель 7fe67bd0a2
Коммит 4efb122f4e
9 изменённых файлов: 583 добавлений и 8 удалений

38
dist/powerbi-client.d.ts поставляемый
Просмотреть файл

@ -419,6 +419,7 @@ declare module "ifilterable" {
}
declare module "visualDescriptor" {
import * as models from 'powerbi-models';
import { IFilterable } from "ifilterable";
import { IPageNode } from "page";
/**
* A Visual node within a report hierarchy
@ -440,7 +441,7 @@ declare module "visualDescriptor" {
* @class VisualDescriptor
* @implements {IVisualNode}
*/
export class VisualDescriptor implements IVisualNode {
export class VisualDescriptor implements IVisualNode, IFilterable {
/**
* The visual name
*
@ -472,6 +473,39 @@ declare module "visualDescriptor" {
*/
page: IPageNode;
constructor(page: IPageNode, name: string, title: string, type: string, layout: models.IVisualLayout);
/**
* Gets all visual level filters of the current visual.
*
* ```javascript
* visual.getFilters()
* .then(filters => { ... });
* ```
*
* @returns {(Promise<models.IFilter[]>)}
*/
getFilters(): Promise<models.IFilter[]>;
/**
* Removes all filters from the current visual.
*
* ```javascript
* visual.removeFilters();
* ```
*
* @returns {Promise<void>}
*/
removeFilters(): Promise<void>;
/**
* Sets the filters on the current visual to 'filters'.
*
* ```javascript
* visual.setFilters(filters);
* .catch(errors => { ... });
* ```
*
* @param {(models.IFilter[])} filters
* @returns {Promise<void>}
*/
setFilters(filters: models.IFilter[]): Promise<void>;
}
}
declare module "page" {
@ -536,7 +570,7 @@ declare module "page" {
*
* ```javascript
* page.getFilters()
* .then(pages => { ... });
* .then(filters => { ... });
* ```
*
* @returns {(Promise<models.IFilter[]>)}

56
dist/powerbi.js поставляемый
Просмотреть файл

@ -142,6 +142,15 @@ return /******/ (function(modules) { // webpackBootstrap
};
_this.handleEvent(event);
});
this.router.post("/reports/:uniqueId/pages/:pageName/visuals/:visualName/events/:eventName", function (req, res) {
var event = {
type: 'report',
id: req.params.uniqueId,
name: req.params.eventName,
value: req.body
};
_this.handleEvent(event);
});
this.router.post("/dashboards/:uniqueId/events/:eventName", function (req, res) {
var event = {
type: 'dashboard',
@ -3509,7 +3518,7 @@ return /******/ (function(modules) { // webpackBootstrap
*
* ```javascript
* page.getFilters()
* .then(pages => { ... });
* .then(filters => { ... });
* ```
*
* @returns {(Promise<models.IFilter[]>)}
@ -3615,6 +3624,51 @@ return /******/ (function(modules) { // webpackBootstrap
this.layout = layout;
this.page = page;
}
/**
* Gets all visual level filters of the current visual.
*
* ```javascript
* visual.getFilters()
* .then(filters => { ... });
* ```
*
* @returns {(Promise<models.IFilter[]>)}
*/
VisualDescriptor.prototype.getFilters = function () {
return this.page.report.service.hpm.get("/report/pages/" + this.page.name + "/visuals/" + this.name + "/filters", { uid: this.page.report.config.uniqueId }, this.page.report.iframe.contentWindow)
.then(function (response) { return response.body; }, function (response) {
throw response.body;
});
};
/**
* Removes all filters from the current visual.
*
* ```javascript
* visual.removeFilters();
* ```
*
* @returns {Promise<void>}
*/
VisualDescriptor.prototype.removeFilters = function () {
return this.setFilters([]);
};
/**
* Sets the filters on the current visual to 'filters'.
*
* ```javascript
* visual.setFilters(filters);
* .catch(errors => { ... });
* ```
*
* @param {(models.IFilter[])} filters
* @returns {Promise<void>}
*/
VisualDescriptor.prototype.setFilters = function (filters) {
return this.page.report.service.hpm.put("/report/pages/" + this.page.name + "/visuals/" + this.name + "/filters", filters, { uid: this.page.report.config.uniqueId }, this.page.report.iframe.contentWindow)
.catch(function (response) {
throw response.body;
});
};
return VisualDescriptor;
}());
exports.VisualDescriptor = VisualDescriptor;

6
dist/powerbi.min.js поставляемый

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

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

@ -70,7 +70,7 @@ export class Page implements IPageNode, IFilterable {
*
* ```javascript
* page.getFilters()
* .then(pages => { ... });
* .then(filters => { ... });
* ```
*
* @returns {(Promise<models.IFilter[]>)}

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

@ -145,6 +145,17 @@ export class Service implements IService {
this.handleEvent(event);
});
this.router.post(`/reports/:uniqueId/pages/:pageName/visuals/:visualName/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(`/dashboards/:uniqueId/events/:eventName`, (req, res) => {
const event: IEvent<any> = {
type: 'dashboard',

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

@ -23,7 +23,7 @@ export interface IVisualNode {
* @class VisualDescriptor
* @implements {IVisualNode}
*/
export class VisualDescriptor implements IVisualNode {
export class VisualDescriptor implements IVisualNode, IFilterable {
/**
* The visual name
*
@ -66,4 +66,53 @@ export class VisualDescriptor implements IVisualNode {
this.layout = layout;
this.page = page;
}
/**
* Gets all visual level filters of the current visual.
*
* ```javascript
* visual.getFilters()
* .then(filters => { ... });
* ```
*
* @returns {(Promise<models.IFilter[]>)}
*/
getFilters(): Promise<models.IFilter[]> {
return this.page.report.service.hpm.get<models.IFilter[]>(`/report/pages/${this.page.name}/visuals/${this.name}/filters`, { uid: this.page.report.config.uniqueId }, this.page.report.iframe.contentWindow)
.then(response => response.body,
response => {
throw response.body;
});
}
/**
* Removes all filters from the current visual.
*
* ```javascript
* visual.removeFilters();
* ```
*
* @returns {Promise<void>}
*/
removeFilters(): Promise<void> {
return this.setFilters([]);
}
/**
* Sets the filters on the current visual to 'filters'.
*
* ```javascript
* visual.setFilters(filters);
* .catch(errors => { ... });
* ```
*
* @param {(models.IFilter[])} filters
* @returns {Promise<void>}
*/
setFilters(filters: models.IFilter[]): Promise<void> {
return this.page.report.service.hpm.put<models.IError[]>(`/report/pages/${this.page.name}/visuals/${this.name}/filters`, filters, { uid: this.page.report.config.uniqueId }, this.page.report.iframe.contentWindow)
.catch(response => {
throw response.body;
});
}
}

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

@ -5,6 +5,7 @@ import * as visual from '../src/visual';
import * as create from '../src/create';
import * as dashboard from '../src/dashboard';
import * as page from '../src/page';
import * as visualDescriptor from '../src/visualDescriptor';
import * as Wpmp from 'window-post-message-proxy';
import * as Hpm from 'http-post-message';
import * as Router from 'powerbi-router';
@ -862,6 +863,11 @@ describe('Protocol', function () {
res.send(202);
});
router.post('/reports/:uniqueId/pages/:pageName/visuals/:visualName/events/:eventName', (req, res) => {
handler.handle(req);
res.send(202);
});
handler = {
test: jasmine.createSpy("testSpy").and.returnValue(true),
handle: jasmine.createSpy("handleSpy").and.callFake(function (message: any) {
@ -1805,6 +1811,7 @@ describe('Protocol', function () {
expect(spyApp.setFilters).not.toHaveBeenCalled();
expect(response.statusCode).toEqual(400);
// Cleanup
spyApp.validatePage.calls.reset();
spyApp.validateFilter.calls.reset();
done();
});
@ -1833,6 +1840,7 @@ describe('Protocol', function () {
expect(spyApp.setFilters).toHaveBeenCalledWith(testData.filters);
expect(response.statusCode).toEqual(202);
// Cleanup
spyApp.validatePage.calls.reset();
spyApp.validateFilter.calls.reset();
spyApp.setFilters.calls.reset();
done();
@ -1870,6 +1878,164 @@ describe('Protocol', function () {
expect(response.statusCode).toEqual(202);
expect(spyHandler.handle).toHaveBeenCalledWith(jasmine.objectContaining(testExpectedEvent));
// Cleanup
spyApp.validatePage.calls.reset();
spyApp.validateFilter.calls.reset();
spyApp.setFilters.calls.reset();
done();
});
});
});
});
describe('filters (visual level)', function () {
it('GET /report/pages/xyz/visuals/uvw/filters returns 200 with body as array of filters', function (done) {
// Arrange
const testData = {
filters: [
{
name: "fakeFilter1"
},
{
name: "fakeFilter2"
}
]
};
iframeLoaded
.then(() => {
spyApp.getFilters.and.returnValue(Promise.resolve(testData.filters));
// Act
hpm.get<models.IFilter[]>('/report/pages/xyz/visuals/uvw/filters')
.then(response => {
// Assert
expect(spyApp.getFilters).toHaveBeenCalled();
expect(response.statusCode).toEqual(200);
expect(response.body).toEqual(testData.filters);
// Cleanup
spyApp.getFilters.calls.reset();
spyApp.validateVisual.calls.reset();
done();
});
});
});
it('GET /report/pages/xyz/visuals/uvw/filters returns 500 with body as error', function (done) {
// Arrange
const testData = {
error: {
message: "internal error"
}
};
iframeLoaded
.then(() => {
spyApp.getFilters.and.returnValue(Promise.reject(testData.error));
// Act
hpm.get<models.IFilter[]>('/report/pages/xyz/visuals/uvw/filters')
.catch(response => {
// Assert
expect(spyApp.getFilters).toHaveBeenCalled();
expect(response.statusCode).toEqual(500);
expect(response.body).toEqual(testData.error);
// Cleanup
spyApp.getFilters.calls.reset();
spyApp.validateVisual.calls.reset();
done();
});
});
});
it('PUT /report/pages/xyz/visuals/uvw/filters returns 400 if request is invalid', function (done) {
// Arrange
const testData = {
uniqueId: 'uniqueId',
filters: [
{
name: "fakeFilter"
}
]
};
iframeLoaded
.then(() => {
spyApp.validateFilter.and.returnValue(Promise.reject(null));
// Act
hpm.put<models.IError>('/report/pages/xyz/visuals/uvw/filters', testData.filters, { uid: testData.uniqueId })
.catch(response => {
// Assert
expect(spyApp.validateFilter).toHaveBeenCalledWith(testData.filters[0]);
expect(spyApp.setFilters).not.toHaveBeenCalled();
expect(response.statusCode).toEqual(400);
// Cleanup
spyApp.validateVisual.calls.reset();
spyApp.validateFilter.calls.reset();
done();
});
});
});
it('PUT /report/pages/xyz/visuals/uvw/filters returns 202 if request is valid', function (done) {
// Arrange
const testData = {
uniqueId: 'uniqueId',
filters: [
{
name: "fakeFilter"
}
],
};
iframeLoaded
.then(() => {
spyApp.validateFilter.and.returnValue(Promise.resolve(null));
// Act
hpm.put<void>('/report/pages/xyz/visuals/uvw/filters', testData.filters, { uid: testData.uniqueId })
.then(response => {
// Assert
expect(spyApp.validateFilter).toHaveBeenCalledWith(testData.filters[0]);
expect(spyApp.setFilters).toHaveBeenCalledWith(testData.filters);
expect(response.statusCode).toEqual(202);
// Cleanup
spyApp.validateVisual.calls.reset();
spyApp.validateFilter.calls.reset();
spyApp.setFilters.calls.reset();
done();
});
});
});
it('PUT /report/:uniqueId/pages/xyz/visuals/uvw/filters will cause POST /reports/:uniqueId/pages/xyz/visuals/uvw/events/filtersApplied', function (done) {
// Arrange
const testData = {
uniqueId: 'uniqueId',
filters: [
{
name: "fakeFilter"
}
]
};
const testExpectedEvent = {
method: 'POST',
url: `/reports/${testData.uniqueId}/pages/xyz/visuals/uvw/events/filtersApplied`
};
iframeLoaded
.then(() => {
// Act
hpm.put<void>('/report/pages/xyz/visuals/uvw/filters', testData.filters, { uid: testData.uniqueId })
.then(response => {
// Assert
expect(spyApp.validateFilter).toHaveBeenCalledWith(testData.filters[0]);
expect(spyApp.setFilters).toHaveBeenCalledWith(testData.filters);
expect(response.statusCode).toEqual(202);
expect(spyHandler.handle).toHaveBeenCalledWith(jasmine.objectContaining(testExpectedEvent));
// Cleanup
spyApp.validateVisual.calls.reset();
spyApp.validateFilter.calls.reset();
spyApp.setFilters.calls.reset();
done();
@ -2098,6 +2264,46 @@ describe('Protocol', function () {
});
});
describe('filters (visual level)', function () {
it('POST /reports/:uniqueId/pages/xyz/visuals/uvw/events/filtersApplied when user changes filter', function (done) {
// Arrange
const testData = {
uniqueId: 'uniqueId',
reportId: 'fakeReportId',
event: {
initiator: 'user',
filters: [
{
name: "fakeFilter"
}
]
}
};
const testExpectedRequest = {
method: 'POST',
url: `/reports/${testData.uniqueId}/pages/xyz/visuals/uvw/events/filtersApplied`,
body: testData.event
};
iframeLoaded
.then(() => {
spyHandler.handle.calls.reset();
// Act
iframeHpm.post(testExpectedRequest.url, testData.event)
.then(response => {
// Assert
expect(response.statusCode).toBe(202);
expect(spyHandler.handle).toHaveBeenCalledWith(jasmine.objectContaining(testExpectedRequest));
done();
});
// Cleanup
});
});
});
describe('settings', function () {
it('POST /reports/:uniqueId/events/settingsUpdated when user changes settings', function (done) {
// Arrange
@ -2187,6 +2393,7 @@ describe('SDK-to-HPM', function () {
let dashboard: dashboard.Dashboard;
let embeddedVisual: visual.Visual;
let page1: page.Page;
let visual1: visualDescriptor.VisualDescriptor;
let uniqueId = 'uniqueId';
let createUniqueId = 'uniqueId';
let dashboardUniqueId = 'uniqueId';
@ -2253,6 +2460,7 @@ describe('SDK-to-HPM', function () {
dashboard = <dashboard.Dashboard>powerbi.embed($dashboardElement[0], dashboardEmbedConfiguration);
embeddedVisual = <visual.Visual>powerbi.embed($visualElement[0], visualEmbedConfiguration);
page1 = new page.Page(report, 'xyz');
visual1 = new visualDescriptor.VisualDescriptor(page1, 'uvw', 'title', 'type', {});
uniqueId = report.config.uniqueId;
createUniqueId = create.config.uniqueId;
dashboardUniqueId = dashboard.config.uniqueId;
@ -3317,6 +3525,159 @@ describe('SDK-to-HPM', function () {
});
});
describe('visual', function () {
describe('filters', function () {
it('visual.getFilters() sends GET /report/pages/xyz/visuals/uvw/filters', function () {
// Arrange
// Act
visual1.getFilters();
// Assert
expect(spyHpm.get).toHaveBeenCalledWith(`/report/pages/${page1.name}/visuals/${visual1.name}/filters`, { uid: uniqueId }, iframe.contentWindow);
});
it('visual.getFilters() return promise that rejects with server error if there was error getting filters', function (done) {
// Arrange
const testData = {
expectedError: {
body: {
message: 'internal server error'
}
}
};
spyHpm.get.and.returnValue(Promise.reject(testData.expectedError));
// Act
visual1.getFilters()
.catch(error => {
// Assert
expect(spyHpm.get).toHaveBeenCalledWith(`/report/pages/${page1.name}/visuals/${visual1.name}/filters`, { uid: uniqueId }, iframe.contentWindow);
expect(error).toEqual(testData.expectedError.body);
done();
});
});
it('visual.getFilters() returns promise that resolves with list of filters', function (done) {
// Arrange
const testData = {
expectedResponse: {
body: [
{ x: 'fakeFilter1' },
{ x: 'fakeFilter2' }
]
}
};
spyHpm.get.and.returnValue(Promise.resolve(testData.expectedResponse));
// Act
visual1.getFilters()
.then(filters => {
// Assert
expect(spyHpm.get).toHaveBeenCalledWith(`/report/pages/${page1.name}/visuals/${visual1.name}/filters`, { uid: uniqueId }, iframe.contentWindow);
expect(filters).toEqual(testData.expectedResponse.body);
done();
});
});
it('visual.setFilters(filters) sends PUT /report/pages/xyz/visuals/uvw/filters', function () {
// Arrange
const testData = {
filters: [
(new models.BasicFilter({ table: "Cars", measure: "Make" }, "In", ["subaru", "honda"])).toJSON(),
(new models.AdvancedFilter({ table: "Cars", measure: "Make" }, "And", [{ value: "subaru", operator: "None" }, { value: "honda", operator: "Contains" }])).toJSON()
],
response: {
body: []
}
};
spyHpm.put.and.returnValue(Promise.resolve(testData.response));
// Act
visual1.setFilters(testData.filters);
// Assert
expect(spyHpm.put).toHaveBeenCalledWith(`/report/pages/${page1.name}/visuals/${visual1.name}/filters`, testData.filters, { uid: uniqueId }, iframe.contentWindow);
});
it('visual.setFilters(filters) returns promise that rejects with validation errors if filter is invalid', function (done) {
// Arrange
const testData = {
filters: [
(new models.BasicFilter({ table: "Cars", measure: "Make" }, "In", ["subaru", "honda"])).toJSON(),
(new models.AdvancedFilter({ table: "Cars", measure: "Make" }, "And", [{ value: "subaru", operator: "None" }, { value: "honda", operator: "Contains" }])).toJSON()
],
expectedErrors: {
body: [
{
message: 'target is invalid, missing property x'
}
]
}
};
spyHpm.put.and.returnValue(Promise.reject(testData.expectedErrors));
// Act
visual1.setFilters(testData.filters)
.catch(errors => {
// Assert
expect(spyHpm.put).toHaveBeenCalledWith(`/report/pages/${page1.name}/visuals/${visual1.name}/filters`, testData.filters, { uid: uniqueId }, iframe.contentWindow);
expect(errors).toEqual(jasmine.objectContaining(testData.expectedErrors.body));
done();
});
});
it('visual.setFilters(filters) returns promise that resolves with null if filter was valid and request is accepted', function (done) {
// Arrange
const testData = {
filters: [
(new models.BasicFilter({ table: "Cars", measure: "Make" }, "In", ["subaru", "honda"])).toJSON(),
(new models.AdvancedFilter({ table: "Cars", measure: "Make" }, "And", [{ value: "subaru", operator: "None" }, { value: "honda", operator: "Contains" }])).toJSON()
]
};
spyHpm.put.and.returnValue(Promise.resolve(null));
// Act
visual1.setFilters(testData.filters)
.then(response => {
// Assert
expect(spyHpm.put).toHaveBeenCalledWith(`/report/pages/${page1.name}/visuals/${visual1.name}/filters`, testData.filters, { uid: uniqueId }, iframe.contentWindow);
expect(response).toEqual(null);
done();
});
});
it('visual.removeFilters() sends PUT /report/pages/xyz/visuals/uvw/filters', function () {
// Arrange
// Act
visual1.removeFilters();
// Assert
expect(spyHpm.put).toHaveBeenCalledWith(`/report/pages/${page1.name}/visuals/${visual1.name}/filters`, [], { uid: uniqueId }, iframe.contentWindow);
});
it('visual.removeFilters() returns promise that resolves with null if request is accepted', function (done) {
// Arrange
spyHpm.put.and.returnValue(Promise.resolve(null));
// Act
visual1.removeFilters()
.then(response => {
// Assert
expect(spyHpm.put).toHaveBeenCalledWith(`/report/pages/${page1.name}/visuals/${visual1.name}/filters`, [], { uid: uniqueId }, iframe.contentWindow);
expect(response).toEqual(null);
done();
});
});
});
});
describe('SDK-to-Router (Event subscription)', function () {
/**
* This test should likely be moved to mock app section or removed since it is already covered.

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

@ -14,6 +14,8 @@ export interface IApp {
getPages(): Promise<models.IPage>;
setPage(pageName: string): Promise<void>;
validatePage(page: models.IPage): Promise<models.IError[]>;
// Visuals
validateVisual(page: models.IPage, visual: models.IVisual): Promise<models.IError[]>;
// Filters
getFilters(): Promise<models.IFilter[]>;
setFilters(filters: models.IFilter[]): Promise<void>;
@ -43,6 +45,8 @@ export const mockAppSpyObj = {
getPages: jasmine.createSpy("getPages").and.returnValue(Promise.resolve(null)),
setPage: jasmine.createSpy("setPage").and.returnValue(Promise.resolve(null)),
validatePage: jasmine.createSpy("validatePage").and.returnValue(Promise.resolve(null)),
// Visuals
validateVisual: jasmine.createSpy("validateVisual").and.returnValue(Promise.resolve(null)),
// Filters
getFilters: jasmine.createSpy("getFilters").and.returnValue(Promise.resolve(null)),
setFilters: jasmine.createSpy("setFilters").and.returnValue(Promise.resolve(null)),
@ -67,6 +71,7 @@ export const mockAppSpyObj = {
mockAppSpyObj.getPages.calls.reset();
mockAppSpyObj.setPage.calls.reset();
mockAppSpyObj.validatePage.calls.reset();
mockAppSpyObj.validateVisual.calls.reset();
mockAppSpyObj.getFilters.calls.reset();
mockAppSpyObj.setFilters.calls.reset();
mockAppSpyObj.validateFilter.calls.reset();

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

@ -259,6 +259,67 @@ export function setupEmbedMockApp(iframeContentWindow: Window, parentWindow: Win
});
});
router.get('/report/pages/:pageName/visuals/:visualName/filters', (req, res) => {
const page = {
name: req.params.pageName,
displayName: null
};
const visual: models.IVisual = {
name: req.params.visualName,
title: 'title',
type: 'type',
layout: {},
};
return app.validateVisual(page, visual)
.then(() => {
return app.getFilters()
.then(filters => {
res.send(200, filters);
}, error => {
res.send(500, error);
});
}, errors => {
res.send(400, errors);
});
});
router.put('/report/pages/:pageName/visuals/:visualName/filters', (req, res) => {
const pageName = req.params.pageName;
const visualName = req.params.visualName;
const uniqueId = req.headers['uid'];
const filters = req.body;
const page: models.IPage = {
name: pageName,
displayName: null
};
const visual: models.IVisual = {
name: visualName,
title: 'title',
type: 'type',
layout: {},
};
return app.validateVisual(page, visual)
.then(() => Promise.all(filters.map(filter => app.validateFilter(filter))))
.then(() => {
app.setFilters(filters)
.then(filter => {
const initiator = "sdk";
hpm.post(`/reports/${uniqueId}/pages/${pageName}/visuals/${visualName}/events/filtersApplied`, {
initiator,
filter
});
}, error => {
hpm.post(`/reports/${uniqueId}/events/error`, error);
});
res.send(202);
}, errors => {
res.send(400, errors);
});
});
router.patch('/report/settings', (req, res) => {
const uniqueId = req.headers['uid'];
const settings = req.body;