acaprojects/powerbi-responsive

View on GitHub
src/report.ts

Summary

Maintainability
A
0 mins
Test Coverage
import { models, Report, IEmbedConfiguration } from 'powerbi-client';
import { IFilter } from 'powerbi-models';
import ResizeObserver from 'resize-observer-polyfill';
import { compose } from 'fp-ts/lib/function';
import { Option } from 'fp-ts/lib/Option';
import { mapOption } from 'fp-ts/lib/Array';
import { fromArray } from 'fp-ts/lib/NonEmptyArray';
import { embed } from './embedder';
import { groupViews } from './view';
import { createResponsivePage, ResponsivePage, activate, isActive } from './page';
import { merge, extend, bind, find, mapL, mapP, bindP, getOrThrow, debounce } from './utils';

/**
 * A set of functions that may be used to interact with an embedded report.
 */
export interface ReportActions {
    setPage(name: string): Promise<void>;
    getPages(): Promise<ResponsivePage[]>;
    getPage(): Promise<ResponsivePage>;
    getFilters(): Promise<IFilter[]>;
    setFilters(filters: IFilter[]): Promise<void>;
    reload(): Promise<void>;
    setAccessToken(token: string): Promise<void>;
    fullscreen(): void;
    exitFullscreen(): void;
}

/**
 * Base embedder config.
 */
const baseConfig: IEmbedConfiguration = {
    type: 'report',
    embedUrl: 'https://app.powerbi.com/reportEmbed',
    tokenType: models.TokenType.Embed,
    permissions: models.Permissions.Read,
    settings: {
        filterPaneEnabled: false,
        navContentPaneEnabled: false
    }
};

/**
 * Embed a view-only, reponsive report in the specified element.
 *
 * The report should be formed with mutliple pages, providing appropriate
 * layouts for different view sizes.
 */
export const embedReport = (id: string, accessToken: string, container: HTMLElement, opts: IEmbedConfiguration = {}) =>
    embed<Report>(container, merge(baseConfig, opts, {id, accessToken}))
        .then(registerEvents)
        .then(bindActions);

/**
 * Get a list of all pages within the report.
 */
const getPages = (report: Report) =>
    bind(report, report.getPages)()
        .then(groupViews)
        .then(mapOption(fromArray))
        .then(mapL(createResponsivePage));

/**
 * Lookup a page within a report by name.
 */
const getPage = (report: Report) => (name: string) =>
    getPages(report)
        .then(find(p => p.name === name))
        .then(getOrThrow(`Could not find page titled ${name}`));

/**
 * Set the page within a responsive report.
 */
const setPage = (report: Report) => compose(
    bindP(activate),
    getPage(report)
);

/**
 * Get the currently active page within a report.
 */
const getActivePage = compose(
    mapP<Option<ResponsivePage>, ResponsivePage>(
        // Should never happen, but just in case...
        getOrThrow('Could not find active page')
    ),
    mapP(find(isActive)),
    getPages
);

/**
 * Reactivate the current page, selecting the most appopriate view for the
 * given state.
 */
const resetView = compose(
    bindP(activate),
    getActivePage
);

/**
 * Expose a set of actions on a reponsive report that are safe to be passed to
 * the outside world.
 */
const bindActions: (report: Report) => ReportActions = report => ({
    setPage: setPage(report),
    getPages: () => getPages(report),
    getPage: () => getActivePage(report),
    getFilters: bind(report, report.getFilters),
    setFilters: bind(report, report.setFilters),
    reload: bind(report, report.reload),
    setAccessToken: bind(report, report.setAccessToken),
    fullscreen: bind(report, report.fullscreen),
    exitFullscreen: bind(report, report.exitFullscreen)
});

/**
 * Bind to resize events on the element that's housing a report and switch in
 * alternative layouts as required.
 */
const registerEvents = (report: Report) => {
    const ro = new ResizeObserver(debounce(() => resetView(report)));
    ro.observe(report.iframe);

    return extend(report, {__resizeObserver: ro});
};