Skip to content

Commit 2f1b8bd

Browse files
authored
Merge pull request #2055 from IFRCGo/feature/embed-pbi
Power BI authenticated embedding - v0.1
2 parents 2589b92 + 5f001b1 commit 2f1b8bd

File tree

10 files changed

+238
-1
lines changed

10 files changed

+238
-1
lines changed

.changeset/six-camels-design.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"go-web-app": patch
3+
---
4+
5+
A demo to embed a Power BI report which is visible to logged in users only

app/env.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export default defineConfig({
2121
APP_TINY_API_KEY: Schema.string(),
2222
APP_RISK_API_ENDPOINT: Schema.string({ format: 'url', protocol: true }),
2323
APP_SDT_URL: Schema.string.optional({ format: 'url', protocol: true, tld: false }),
24+
APP_POWER_BI_REPORT_ID_1: Schema.string.optional(),
2425
APP_SENTRY_DSN: Schema.string.optional(),
2526
APP_SENTRY_TRACES_SAMPLE_RATE: Schema.number.optional(),
2627
APP_SENTRY_REPLAYS_SESSION_SAMPLE_RATE: Schema.number.optional(),

app/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
"html-to-image": "^1.11.11",
5454
"mapbox-gl": "^1.13.0",
5555
"papaparse": "^5.4.1",
56+
"powerbi-client": "^2.23.1",
5657
"react": "^18.2.0",
5758
"react-dom": "^18.2.0",
5859
"react-router-dom": "^6.18.0",

app/src/App/routes/index.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -741,6 +741,20 @@ const resources = customWrapRoute({
741741
visibility: 'anything',
742742
},
743743
});
744+
745+
const spark = customWrapRoute({
746+
parent: rootLayout,
747+
path: 'spark',
748+
component: {
749+
render: () => import('#views/Spark'),
750+
props: {},
751+
},
752+
wrapperComponent: Auth,
753+
context: {
754+
title: 'SPARK',
755+
visibility: 'is-authenticated',
756+
},
757+
});
744758
const operationalLearning = customWrapRoute({
745759
parent: rootLayout,
746760
path: 'operational-learning',
@@ -1317,6 +1331,7 @@ const wrappedRoutes = {
13171331
accountMyFormsDref,
13181332
accountMyFormsThreeW,
13191333
resources,
1334+
spark,
13201335
search,
13211336
allThreeWProject,
13221337
allThreeWActivity,
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import {
2+
useEffect,
3+
useRef,
4+
} from 'react';
5+
import { _cs } from '@togglecorp/fujs';
6+
import {
7+
factories,
8+
type IEmbedConfiguration,
9+
models,
10+
service,
11+
} from 'powerbi-client';
12+
13+
import styles from './styles.module.css';
14+
15+
type PowerBiEmbedProps = {
16+
embedUrl: string;
17+
accessToken: string;
18+
reportId?: string;
19+
className?: string;
20+
};
21+
22+
function PowerBiEmbed(props: PowerBiEmbedProps) {
23+
const {
24+
embedUrl,
25+
accessToken,
26+
reportId,
27+
className,
28+
} = props;
29+
30+
const containerRef = useRef<HTMLDivElement | null>(null);
31+
32+
useEffect(() => {
33+
const element = containerRef.current;
34+
if (!element) {
35+
return undefined;
36+
}
37+
if (!embedUrl || !accessToken) {
38+
// Do not attempt to embed without required fields
39+
return undefined;
40+
}
41+
42+
const powerBiService = new service.Service(
43+
factories.hpmFactory,
44+
factories.wpmpFactory,
45+
factories.routerFactory,
46+
);
47+
48+
const config: IEmbedConfiguration = {
49+
type: 'report',
50+
id: reportId,
51+
embedUrl,
52+
accessToken,
53+
tokenType: models.TokenType.Embed,
54+
settings: {
55+
panes: {
56+
filters: { visible: false },
57+
},
58+
layoutType: models.LayoutType.FitToWidth,
59+
// Enable/disable parts of the UI as needed
60+
navContentPaneEnabled: true,
61+
},
62+
};
63+
64+
// Embed the report
65+
powerBiService.embed(element, config);
66+
67+
// Cleanup on unmount
68+
return () => {
69+
try {
70+
powerBiService.reset(element as HTMLElement);
71+
} catch {
72+
// ignore
73+
}
74+
};
75+
}, [embedUrl, accessToken, reportId]);
76+
77+
return (
78+
<div
79+
className={_cs(styles.sparkEmbed, className)}
80+
ref={containerRef}
81+
/>
82+
);
83+
}
84+
85+
export default PowerBiEmbed;
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
.spark-embed {
2+
aspect-ratio: 5 / 3;
3+
4+
iframe {
5+
border: none;
6+
}
7+
}

app/src/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const {
77
APP_TINY_API_KEY,
88
APP_RISK_API_ENDPOINT,
99
APP_SDT_URL,
10+
APP_POWER_BI_REPORT_ID_1,
1011
APP_SENTRY_DSN,
1112
APP_SENTRY_TRACES_SAMPLE_RATE,
1213
APP_SENTRY_REPLAYS_SESSION_SAMPLE_RATE,
@@ -30,6 +31,7 @@ export const adminUrl = APP_ADMIN_URL ?? `${api}admin/`;
3031
export const mbtoken = APP_MAPBOX_ACCESS_TOKEN;
3132
export const riskApi = APP_RISK_API_ENDPOINT;
3233
export const sdtUrl = APP_SDT_URL;
34+
export const powerBiReportId1 = APP_POWER_BI_REPORT_ID_1;
3335

3436
export const tinyApiKey = APP_TINY_API_KEY;
3537
export const sentryAppDsn = APP_SENTRY_DSN;

app/src/views/Spark/index.tsx

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { Container } from '@ifrc-go/ui';
2+
import {
3+
isDefined,
4+
isNotDefined,
5+
} from '@togglecorp/fujs';
6+
7+
import SparkEmbed from '#components/domain/SparkEmbed';
8+
import Page from '#components/Page';
9+
import { powerBiReportId1 } from '#config';
10+
import { useRequest } from '#utils/restRequest';
11+
12+
// Backend returns snake_case keys
13+
type BackendPowerBiAuth = {
14+
embed_url: string;
15+
embed_token: string;
16+
report_id?: string;
17+
expires_at?: string; // new field for expiry (ISO string)
18+
};
19+
20+
/** @knipignore */
21+
// eslint-disable-next-line import/prefer-default-export
22+
export function Component() {
23+
const {
24+
response: authRaw,
25+
pending,
26+
error,
27+
} = useRequest({
28+
skip: !powerBiReportId1,
29+
url: '/api/v2/auth-power-bi/',
30+
preserveResponse: true,
31+
query: powerBiReportId1 ? ({
32+
report_id: powerBiReportId1,
33+
}) : undefined,
34+
});
35+
36+
// FIXME: the typings should be generated in the server
37+
const auth = authRaw as unknown as BackendPowerBiAuth | undefined;
38+
const embedUrl = auth?.embed_url;
39+
const accessToken = auth?.embed_token;
40+
const reportId = auth?.report_id;
41+
42+
return (
43+
<Page
44+
// FIXME: use strings
45+
title="SPARK"
46+
// FIXME: use strings
47+
heading="SPARK"
48+
>
49+
<Container
50+
pending={pending}
51+
errored={!!error}
52+
errorMessage={error?.value.messageForNotification}
53+
empty={isNotDefined(powerBiReportId1)
54+
|| isNotDefined(embedUrl)
55+
|| isNotDefined(accessToken)}
56+
// FIXME: use strings
57+
emptyMessage="Page not available!"
58+
>
59+
{isDefined(embedUrl) && isDefined(accessToken) && (
60+
<SparkEmbed
61+
embedUrl={embedUrl}
62+
accessToken={accessToken}
63+
reportId={reportId}
64+
/>
65+
)}
66+
</Container>
67+
</Page>
68+
);
69+
}
70+
71+
Component.displayName = 'Spark';

pnpm-lock.yaml

Lines changed: 50 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)