Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: GEO-1199 Load PowerBi by report name #839

Merged
merged 2 commits into from
Nov 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 37 additions & 23 deletions admin-frontend/src/components/AnalyticsPage.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<template>
<div v-if="isAnalyticsAvailable" class="w-100 overflow-x-auto">
<div
v-for="[name, details] in resourceDetails"
v-for="[name, details] in powerBiDetailsPerResource"
:key="name"
class="powerbi-container"
>
Expand Down Expand Up @@ -36,12 +36,12 @@ type PowerBiDetails = {
const isAnalyticsAvailable =
(window as any).config?.IS_ADMIN_ANALYTICS_AVAILABLE?.toUpperCase() == 'TRUE';

const resourceDetails = createDefaultPowerBiDetailsMap([
const powerBiDetailsPerResource = createDefaultPowerBiDetailsMap([
POWERBI_RESOURCE.ANALYTICS,
]);

if (isAnalyticsAvailable) {
getPowerBiAccessToken(resourceDetails);
getPowerBiAccessToken(powerBiDetailsPerResource);
}

/** Create a Map containing the details of the resources. */
Expand All @@ -60,31 +60,13 @@ function createDefaultPowerBiDetailsMap(
accessToken: undefined,
tokenType: models.TokenType.Embed,
},
css: { width: '200px', height: '400px' },
css: { width: '1280px', height: '720px' },
// eventHandlersMap - https://learn.microsoft.com/en-us/javascript/api/overview/powerbi/handle-events#report-events
eventHandlersMap: new Map([
[
'loaded', // The loaded event is raised when the report initializes.
() => {
/** Set the css size of the report to be the size of the maximum page of all pages. */
const setCssSize = async () => {
const pages = await resourceDetails.get(name)!.report?.getPages();
if (pages) {
const sizes = pages.reduce(
(prev, current) => ({
width: Math.max(prev.width, current.defaultSize.width ?? 0),
height: Math.max(
prev.height,
current.defaultSize.height ?? 0,
),
}),
{ width: 0, height: 0 },
);
resourceDetails.get(name)!.css.width = sizes.width + 'px';
resourceDetails.get(name)!.css.height = sizes.height + 'px';
}
};
setCssSize();
setCssSize(name);
},
],
]),
Expand Down Expand Up @@ -126,6 +108,38 @@ async function getPowerBiAccessToken(
const msToExpiry = Duration.between(now, expiry).minusMinutes(1).toMillis();
setTimeout(getPowerBiAccessToken, msToExpiry);
}

/**
* Set the css size of the report to be the size of the maximum page of all pages.
* Warning: The navigation pane is not redrawn when increasing the display size which
* leaves a gray area where there should be the navigation pane. You can manually refresh
* the iframe by right clicking on the gray area and selecting 'refresh frame'.
* @param name
* @param refresh Reloads the iframe. Loading takes longer, but the navigation pane will be drawn correctly
*/
async function setCssSize(name: POWERBI_RESOURCE, refresh: boolean = false) {
const details = powerBiDetailsPerResource.get(name)!;
if (!details.report) return;
const pages = await details.report.getPages();
if (pages) {
const sizes = pages.reduce(
(prev, current) => ({
width: Math.max(prev.width, current.defaultSize.width ?? 0),
height: Math.max(prev.height, current.defaultSize.height ?? 0),
}),
{ width: 0, height: 0 },
);

if (
details.css.width != sizes.width + 'px' ||
details.css.height != sizes.height + 'px'
) {
details.css.width = sizes.width + 'px';
details.css.height = sizes.height + 'px';
if (refresh) details.report.iframe.src += ''; // Allegedly, this is how to refresh an iframe
}
}
}
</script>

<style lang="scss">
Expand Down
19 changes: 16 additions & 3 deletions backend/src/external/services/powerbi-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,21 @@ export class Api {
body,
config,
);

/** https://learn.microsoft.com/en-us/rest/api/power-bi/reports/get-reports */
public getReports = (
url: Group_Url,
config: AxiosRequestConfig,
): Promise<AxiosResponse<Reports>> =>
this.api.get<Reports>(
`/v1.0/myorg/groups/${url.workspaceId}/reports`,
config,
);
}

type DashboardInGroup_Url = { workspaceId: string; dashboardId: string };
type ReportInGroup_Url = { workspaceId: string; reportId: string };
type Group_Url = { workspaceId: string };

/** https://learn.microsoft.com/en-us/rest/api/power-bi/embed-token/dashboards-generate-token-in-group#request-body */
export type GenerateTokenForDashboardInGroup_Body = {
Expand All @@ -82,15 +93,13 @@ export type GenerateToken_Body = {

/** https://learn.microsoft.com/en-us/rest/api/power-bi/embed-token/generate-token#embedtoken */
export type EmbedToken = {
'@odata.context': string;
token: string;
tokenId: string;
expiration: string;
};

/** https://learn.microsoft.com/en-us/rest/api/power-bi/reports/get-report-in-group#report */
export type Report = {
'@odata.context': string;
id: string;
reportType: string;
name: string;
Expand All @@ -104,9 +113,13 @@ export type Report = {
subscriptions: [];
};

/** https://learn.microsoft.com/en-us/rest/api/power-bi/reports/get-reports#reports */
export type Reports = {
value: Report[];
};

/** https://learn.microsoft.com/en-us/rest/api/power-bi/dashboards/get-dashboard-in-group#dashboard */
export type Dashboard = {
'@odata.context': string;
id: string;
displayName: string;
isReadOnly: boolean;
Expand Down
60 changes: 59 additions & 1 deletion backend/src/external/services/powerbi-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type EmbedConfig = {
};

export type ReportInWorkspace = { workspaceId: string; reportId: string };
export type ReportNameInWorkspace = { workspaceId: string; reportName: string };

/**
* Class to authenticate and make use of the PowerBi REST API.
Expand All @@ -39,6 +40,63 @@ export class PowerBiService {
this.powerBiApi = new PowerBi.Api(powerBiUrl);
}

/**
* Get embed params for multiple report in multiple workspace. Search reports by name
* https://learn.microsoft.com/en-us/rest/api/power-bi/reports/get-report-in-group
* @return EmbedConfig object
*/
public async getEmbedParamsForReportsByName(
reportNameInWorkspace: ReportNameInWorkspace[],
): Promise<EmbedConfig> {
try {
const header = await this.getEntraAuthorizationHeader();

const workspaces = uniq(reportNameInWorkspace.map((x) => x.workspaceId));

const reportsPerWorkspace: Record<string, PowerBi.Report[]> = {};

// Get all the reports in each workspace by calling the PowerBI REST API
await Promise.all(
workspaces.map(async (id) => {
const reports = await this.powerBiApi.getReports(
{ workspaceId: id },
{
headers: header,
},
);
reportsPerWorkspace[id] = reports.data.value;
}),
);

// Limit the found reports to only the ones requested
const reports = reportNameInWorkspace.map((res) =>
reportsPerWorkspace[res.workspaceId].find(
(x) => x.name == res.reportName,
),
);

// Get Embed token multiple resources
const embedToken = await this.getEmbedTokenForV2Workspace(
uniq(reports.map((report) => report.id)),
uniq(reports.map((report) => report.datasetId)),
uniq(reportNameInWorkspace.map((res) => res.workspaceId)),
);

// Add report data for embedding
const reportDetails: PowerBiResource[] = reports.map((report) => ({
id: report.id,
name: report.name,
embedUrl: report.embedUrl,
}));

return { embedToken: embedToken, resources: reportDetails };
} catch (err) {
if (err instanceof AxiosError && err?.response?.data)
err.message = JSON.stringify(err.response.data);
throw err;
}
}

/**
* https://learn.microsoft.com/en-us/rest/api/power-bi/dashboards/get-dashboard-in-group
*/
Expand Down Expand Up @@ -77,7 +135,7 @@ export class PowerBiService {
}

/**
* Get embed params for a single report for a single workspace
* Get embed params for multiple reports in multiple workspace
* https://learn.microsoft.com/en-us/rest/api/power-bi/reports/get-report-in-group
* @return EmbedConfig object
*/
Expand Down
10 changes: 5 additions & 5 deletions backend/src/v1/services/analytic-service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import {
PowerBiResourceName,
} from './analytic-service';

const mockGetEmbedParamsForReports = jest.fn();
const mockgetEmbedParamsForReportsByName = jest.fn();
jest.mock('../../external/services/powerbi-service', () => {
const actual = jest.requireActual('../../external/services/powerbi-service');
return {
...actual,
PowerBiService: jest.fn().mockImplementation(() => {
return {
getEmbedParamsForReports: mockGetEmbedParamsForReports,
getEmbedParamsForReportsByName: mockgetEmbedParamsForReportsByName,
};
}),
};
Expand Down Expand Up @@ -40,7 +40,7 @@ describe('getEmbedInfo', () => {
expiry: '2024',
};

mockGetEmbedParamsForReports.mockResolvedValue({
mockgetEmbedParamsForReportsByName.mockResolvedValue({
resources: [
{ id: output.resources[0].id, embedUrl: output.resources[0].embedUrl },
{ id: output.resources[1].id, embedUrl: output.resources[1].embedUrl },
Expand All @@ -51,7 +51,7 @@ describe('getEmbedInfo', () => {
PowerBiResourceName.Analytics,
PowerBiResourceName.Analytics,
]);
expect(mockGetEmbedParamsForReports).toHaveBeenCalledTimes(1);
expect(mockgetEmbedParamsForReportsByName).toHaveBeenCalledTimes(1);
expect(json).toMatchObject(output);
});

Expand All @@ -62,6 +62,6 @@ describe('getEmbedInfo', () => {
'invalid' as never,
]),
).rejects.toThrow('Invalid resource names');
expect(mockGetEmbedParamsForReports).not.toHaveBeenCalled();
expect(mockgetEmbedParamsForReportsByName).not.toHaveBeenCalled();
});
});
8 changes: 4 additions & 4 deletions backend/src/v1/services/analytic-service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { config } from '../../config';
import {
PowerBiService,
ReportInWorkspace,
ReportNameInWorkspace,
} from '../../external/services/powerbi-service';

// Embed info for Reports, Dashboards, and other resources
Expand All @@ -15,10 +15,10 @@ export enum PowerBiResourceName {
Analytics = 'Analytics',
}

const resourceIds: Record<PowerBiResourceName, ReportInWorkspace> = {
const resourceIds: Record<PowerBiResourceName, ReportNameInWorkspace> = {
Analytics: {
workspaceId: config.get('powerbi:analytics:workspaceId'),
reportId: config.get('powerbi:analytics:analyticsId'),
reportName: config.get('powerbi:analytics:analyticsId'),
},
};

Expand Down Expand Up @@ -46,7 +46,7 @@ export const analyticsService = {
config.get('entra:tenantId'),
);

const embedParams = await powerBi.getEmbedParamsForReports(
const embedParams = await powerBi.getEmbedParamsForReportsByName(
resourceNames.map((name) => resourceIds[name]),
);

Expand Down