Skip to content

Commit

Permalink
Merge branch 'main' into renovate/major-linters
Browse files Browse the repository at this point in the history
  • Loading branch information
sukanya-rath authored Nov 7, 2024
2 parents 856f1e8 + 61307fb commit 72208cb
Show file tree
Hide file tree
Showing 12 changed files with 149 additions and 40 deletions.
1 change: 1 addition & 0 deletions .github/workflows/.deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ jobs:
--set-string crunchy.pgBackRest.s3.accessKey="${{ secrets.S3_ACCESS_KEY }}" \
--set-string crunchy.pgBackRest.s3.secretKey="${{ secrets.S3_SECRET_ACCESS_KEY }}" \
--set-string global.secrets.clamavApiKey="${{ secrets.CLAMAV_API_KEY }}" \
--set-string global.secrets.snowplowUrl="${{ secrets.SNOWPLOW_URL }}" \
${{ inputs.params }} \
--timeout "$DEPLOY_TIMEOUT"m ./${{ github.event.repository.name }}-${{ steps.vars.outputs.semver }}.tgz
Expand Down
3 changes: 2 additions & 1 deletion admin-frontend/Caddyfile
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
}
respond `window.config = {
"IS_ADMIN_DASHBOARD_AVAILABLE":"{$IS_ADMIN_DASHBOARD_AVAILABLE}",
"IS_ADMIN_ANALYTICS_AVAILABLE":"{$IS_ADMIN_ANALYTICS_AVAILABLE}"
"IS_ADMIN_ANALYTICS_AVAILABLE":"{$IS_ADMIN_ANALYTICS_AVAILABLE}",
"SNOWPLOW_URL":"{$SNOWPLOW_URL}"
};`
}
root * /app/dist
Expand Down
4 changes: 4 additions & 0 deletions admin-frontend/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,10 @@ a:hover {
background-color: transparent !important;
}
.v-card-title {
font-size: 1.35rem !important; //default is 1.25, but BCSans font looks wonky at that size.
}
.theme--light.application {
background: #f1f1f1;
}
Expand Down
74 changes: 51 additions & 23 deletions admin-frontend/src/components/AnalyticsPage.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
<template>
<v-btn
v-tooltip:bottom-end="'Only authorized users can access this data'"
append-icon="mdi-open-in-new"
class="ml-auto btn-primary"
style="margin-top: -40px"
rel="noopener"
target="_blank"
:href="sanitizeUrl(snowplowUrl)"
>Web Traffic Analytics</v-btn
>

<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 All @@ -25,6 +36,7 @@ import ApiService from '../services/apiService';
import { ZonedDateTime, Duration } from '@js-joda/core';
import { POWERBI_RESOURCE } from '../utils/constant';
import { NotificationService } from '../services/notificationService';
import { sanitizeUrl } from '@braintree/sanitize-url';
type PowerBiDetails = {
config: IReportEmbedConfiguration;
Expand All @@ -33,15 +45,17 @@ type PowerBiDetails = {
eventHandlersMap: Map<string, EventHandler>;
};
const snowplowUrl = (window as any).config?.SNOWPLOW_URL;
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 +74,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 +122,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
6 changes: 3 additions & 3 deletions backend/src/external/services/s3-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ export const deleteFiles = async (ids: string[]): Promise<Set<string>> => {
try {
// Get all the files stored under each id
const filesPerId = await Promise.all(
ids.map((id) => getFileList(s3Client, id)), //TODO: try deleting the folders instead of each individual file. https://stackoverflow.com/a/73367823
ids.map((id) => getFileList(s3Client, id)),
);
const idsWithNoFiles = ids.filter(
(id, index) => filesPerId[index].length === 0,
Expand All @@ -150,15 +150,15 @@ export const deleteFiles = async (ids: string[]): Promise<Set<string>> => {

// report any errors
responsePerGroup.forEach((r) =>
r.Errors.forEach((e) => {
r.Errors?.forEach((e) => {
if (e.Code == 'NoSuchKey') idsWithNoFiles.push(getIdFromKey(e.Key));
logger.error(e.Message);
}),
);

// Return the id of all successful deleted
const successfulIds = responsePerGroup.flatMap((r) =>
r.Deleted.reduce((acc, x) => {
r.Deleted?.reduce((acc, x) => {
acc.push(getIdFromKey(x.Key));
return acc;
}, [] as string[]),
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
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ spec:
value: "{{ .Values.env.isAdminDashboardAvailable }}"
- name: IS_ADMIN_ANALYTICS_AVAILABLE
value: "{{ .Values.env.isAdminAnalyticsAvailable }}"
- name: SNOWPLOW_URL
value: "{{ .Values.global.secrets.snowplowUrl }}"
- name: CLAMAV_API_KEY
valueFrom:
secretKeyRef:
Expand Down
1 change: 1 addition & 0 deletions charts/fin-pay-transparency/templates/secret.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,5 @@ data:
S3_BUCKET_NAME: {{ .Values.global.secrets.s3Bucket | b64enc | quote }}
S3_ENDPOINT: {{ .Values.global.secrets.s3Endpoint | b64enc | quote }}
CLAMAV_API_KEY: {{ .Values.global.secrets.clamavApiKey | b64enc | quote }}
SNOWPLOW_URL: {{ .Values.global.secrets.snowplowUrl | b64enc | quote }}

Loading

0 comments on commit 72208cb

Please sign in to comment.