Skip to content

Commit

Permalink
Merge pull request #4051 from beyondessential/dev
Browse files Browse the repository at this point in the history
merge latest dev into test branch
  • Loading branch information
avaek authored Jul 28, 2022
2 parents 8d8f589 + 4c0466f commit d76455e
Show file tree
Hide file tree
Showing 102 changed files with 3,224 additions and 1,872 deletions.
5 changes: 5 additions & 0 deletions packages/devops/scripts/deployment/buildDeployablePackages.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ PACKAGES=$(${TUPAIA_DIR}/scripts/bash/getDeployablePackages.sh)
cd ${TUPAIA_DIR}
yarn install --frozen-lockfile

# "postinstall" hook may only fire if the dependency tree changes. This may not happen on feature branches based off dev,
# because our AMI performs a yarn install already. In this case we can end up in a situation where "internal-depenednecies"
# packages' dists are not rebuilt. This will be fixed by changing to a single yarn:build command in a future PR.
yarn build:internal-dependencies

# Inject environment variables from LastPass
LASTPASS_EMAIL=$($DIR/fetchParameterStoreValue.sh LASTPASS_EMAIL)
LASTPASS_PASSWORD=$($DIR/fetchParameterStoreValue.sh LASTPASS_PASSWORD)
Expand Down
40 changes: 40 additions & 0 deletions packages/devops/scripts/deployment/setupGoldMaster.sh
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,46 @@ cd ../
rm -rf lastpass-cli
mkdir -p /home/ubuntu/.local/share/lpass

# install puppeteer dependencies https://pptr.dev/15.3.0/troubleshooting#chrome-headless-doesnt-launch-on-unix
sudo apt-get -y install \
ca-certificates \
fonts-liberation \
libappindicator3-1 \
libasound2 \
libatk-bridge2.0-0 \
libatk1.0-0 \
libc6 \
libcairo2 \
libcups2 \
libdbus-1-3 \
libexpat1 \
libfontconfig1 \
libgbm1 \
libgcc1 \
libglib2.0-0 \
libgtk-3-0 \
libnspr4 \
libnss3 \
libpango-1.0-0 \
libpangocairo-1.0-0 \
libstdc++6 \
libx11-6 \
libx11-xcb1 \
libxcb1 \
libxcomposite1 \
libxcursor1 \
libxdamage1 \
libxext6 \
libxfixes3 \
libxi6 \
libxrandr2 \
libxrender1 \
libxss1 \
libxtst6 \
lsb-release \
wget \
xdg-utils

# clone our repo
cd /home/ubuntu
sudo -Hu ubuntu git clone https://github.com/beyondessential/tupaia.git
Expand Down
1 change: 1 addition & 0 deletions packages/expression-parser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
},
"dependencies": {
"date-fns": "^2.16.1",
"lodash.startcase": "^4.4.0",
"mathjs": "^9.4.0"
},
"devDependencies": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
* Copyright (c) 2017 - 2021 Beyond Essential Systems Pty Ltd
*/

import startCase from 'lodash.startcase';

// We use the `'undefined'` string to indicate a missing value in cases where the
// actual `undefined` JS type cannot be used, e.g. in json config persisted in the DB
const isUndefined = value => value !== undefined && value !== 'undefined';
Expand Down Expand Up @@ -30,9 +32,14 @@ const translate = (value, translations) => {

const date = (...argumentList) => new Date(...argumentList);

const upperFirstCase = value => {
return startCase(value);
};

export const customFunctions = {
avg: average,
firstExistingValue,
translate,
date,
upperFirstCase,
};
2 changes: 2 additions & 0 deletions packages/lesmis-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,11 @@
"@tupaia/server-boilerplate": "1.0.0",
"@tupaia/utils": "1.0.0",
"camelcase-keys": "^6.2.2",
"cookies": "^0.8.0",
"dotenv": "^8.2.0",
"express": "^4.16.2",
"lodash.uniqby": "^4.7.0",
"puppeteer": "^15.4.0",
"winston": "^3.2.1"
}
}
3 changes: 3 additions & 0 deletions packages/lesmis-server/src/app/createApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
UserRoute,
VerifyEmailRoute,
UpdateSurveyResponseRoute,
PDFExportRoute,
} from '../routes';
import { attachSession } from '../session';
import { hasLesmisAccess } from '../utils';
Expand All @@ -31,6 +32,7 @@ import { ReportRequest } from '../routes/ReportRoute';
import { VerifyEmailRequest } from '../routes/VerifyEmailRoute';
import { RegisterRequest } from '../routes/RegisterRoute';
import { UpdateSurveyResponseRequest } from '../routes/UpdateSurveyResponseRoute';
import { PDFExportRequest } from '../routes/PDFExportRoute';

const { CENTRAL_API_URL = 'http://localhost:8090/v2' } = process.env;

Expand Down Expand Up @@ -64,6 +66,7 @@ export function createApp() {

.post<RegisterRequest>('register', handleWith(RegisterRoute))
.post<ReportRequest>('report/:entityCode/:reportCode', handleWith(ReportRoute))
.post<PDFExportRequest>('pdf', handleWith(PDFExportRoute))

/**
* PUT
Expand Down
96 changes: 96 additions & 0 deletions packages/lesmis-server/src/routes/PDFExportRoute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* Tupaia
* Copyright (c) 2017 - 2022 Beyond Essential Systems Pty Ltd
*
*/
import { Request, Response, NextFunction } from 'express';
import { Route } from '@tupaia/server-boilerplate';
import puppeteer from 'puppeteer';
import Cookies from 'cookies';

type SessionCookies = {
sessionCookieName: string;
sessionCookieValue: string;
};

type Body = {
pdfPageUrl: string;
};

export type PDFExportRequest = Request<
Record<string, never>,
{ contents: Buffer },
Body,
Record<string, any>
>;

export class PDFExportRoute extends Route<PDFExportRequest> {
public constructor(req: PDFExportRequest, res: Response, next: NextFunction) {
super(req, res, next);
this.type = 'download';
}

private extractSessionCookie = (): SessionCookies => {
const cookies = new Cookies(this.req, this.res);
const sessionCookieName = 'sessionCookie';
const sessionCookieValue = cookies.get(sessionCookieName) || '';
return { sessionCookieName, sessionCookieValue };
};

private verifyBody = (body: any): Body => {
const { pdfPageUrl } = body;
if (!pdfPageUrl || typeof pdfPageUrl !== 'string') {
throw new Error(`'pdfPageUrl' should be provided in request body, got: ${pdfPageUrl}`);
}
const location = new URL(pdfPageUrl);
if (!location.hostname.endsWith('.tupaia.org') && !location.hostname.endsWith('localhost')) {
throw new Error(`'pdfPageUrl' is not valid, got: ${pdfPageUrl}`);
}
return { pdfPageUrl };
};

private exportPDF = async (): Promise<Buffer> => {
const { sessionCookieName, sessionCookieValue } = this.extractSessionCookie();
const { pdfPageUrl } = this.verifyBody(this.req.body);
const { host: apiDomain } = this.req.headers;
const location = new URL(pdfPageUrl);

let browser;
let result;

try {
browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setCookie({
name: sessionCookieName,
domain: apiDomain,
url: location.origin,
httpOnly: true,
value: sessionCookieValue,
});
await page.goto(pdfPageUrl, { timeout: 60000, waitUntil: 'networkidle0' });
result = await page.pdf({
format: 'a4',
printBackground: true,
});
} catch (e) {
throw new Error(`puppeteer error: ${(e as Error).message}`);
} finally {
if (browser) {
await browser.close();
}
}

return result;
};

public async buildResponse() {
const buffer = await this.exportPDF();
this.res.set({
'Content-Type': 'application/pdf',
'Content-Length': buffer.length,
'Content-Disposition': 'attachment',
});
return { contents: buffer };
}
}
1 change: 1 addition & 0 deletions packages/lesmis-server/src/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export { EntitiesRoute } from './EntitiesRoute';
export { MapOverlaysRoute } from './MapOverlaysRoute';
export { RegisterRoute } from './RegisterRoute';
export { ReportRoute } from './ReportRoute';
export { PDFExportRoute } from './PDFExportRoute';
export { UserRoute } from './UserRoute';
export { UpdateSurveyResponseRoute } from './UpdateSurveyResponseRoute';
export { VerifyEmailRoute } from './VerifyEmailRoute';
9 changes: 6 additions & 3 deletions packages/lesmis/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,12 @@
"lint": "yarn package:lint:js",
"lint:fix": "yarn lint --fix",
"start-dev": "PORT=3003 yarn package:start:react-scripts",
"start-fullstack": "npm-run-all -c -l -p start-central-server start-web-config-server start-dev",
"start-central-server": "yarn workspace @tupaia/central-server start-dev -s",
"start-web-config-server": "yarn workspace @tupaia/web-config-server start-dev -s",
"start-fullstack": "npm-run-all -c -l -p start-central-server start-entity-server start-report-server start-web-config-server start-lesmis-server start-dev",
"start-central-server": "yarn workspace @tupaia/central-server start-dev",
"start-entity-server": "yarn workspace @tupaia/entity-server start-dev",
"start-report-server": "yarn workspace @tupaia/report-server start-dev",
"start-web-config-server": "yarn workspace @tupaia/web-config-server start-dev",
"start-lesmis-server": "yarn workspace @tupaia/lesmis-server start-dev",
"test": "yarn package:test"
},
"browserslist": [
Expand Down
4 changes: 3 additions & 1 deletion packages/lesmis/src/api/queries/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@ export const yearToApiDates = year => {

export const combineQueries = queryObject => {
const queries = Object.values(queryObject);
const error = queries.find(q => q.error)?.error ?? null;

return {
isLoading: !!queries.find(q => q.isLoading),
isFetching: !!queries.find(q => q.isFetching),
error: queries.find(q => q.error)?.error ?? null,
error,
isError: !!error,
data: Object.fromEntries(Object.entries(queryObject).map(([key, q]) => [key, q.data])),
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/*
* Tupaia
* Copyright (c) 2017 - 2022 Beyond Essential Systems Pty Ltd
*/
import React, { useState } from 'react';
import { useIsFetching } from 'react-query';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import DownloadIcon from '@material-ui/icons/GetApp';
import {
Dialog,
DialogHeader,
DialogContent as BaseDialogContent,
FlexSpaceBetween as BaseFlexSpaceBetween,
LoadingContainer,
} from '@tupaia/ui-components';
import MuiIconButton from '@material-ui/core/Button';
import { OptionsBar } from './components';
import { I18n, useDashboardItemsExportToPDF, useUrlParams, useUrlSearchParam } from '../../utils';
import { DEFAULT_DATA_YEAR } from '../../constants';
import { DASHBOARD_EXPORT_PREVIEW, ExportView } from '../../views/ExportView';

const FlexSpaceBetween = styled(BaseFlexSpaceBetween)`
width: 95%;
`;

const DialogContent = styled(BaseDialogContent)`
background-color: white;
`;

const MuiButton = styled(MuiIconButton)`
margin: 0px 20px;
background-color: transparent;
color: #666666;
`;

export const DashboardExportModal = ({ title, totalPage, isOpen, setIsOpen }) => {
const [exportWithLabels, setExportWithLabels] = useState(null);
const [exportWithTable, setExportWithTable] = useState(null);
const [selectedYear] = useUrlSearchParam('year', DEFAULT_DATA_YEAR);
const { locale, entityCode } = useUrlParams();

const toggleExportWithLabels = () => {
setExportWithLabels(exportWithLabels ? null : true);
};
const toggleExportWithTable = () => {
setExportWithTable(exportWithTable ? null : true);
};

const fileName = `${title}-dashboards-export`;
const { isExporting, exportToPDF, errorMessage, onReset } = useDashboardItemsExportToPDF({
exportWithLabels,
exportWithTable,
year: selectedYear,
locale,
entityCode,
});

const isFetching = useIsFetching() > 0;
const isError = !!errorMessage;
const isDisabled = isError || isExporting || isFetching;
const [page, setPage] = useState(1);

const handleClickExport = async () => {
await exportToPDF(fileName);
};
const onClose = () => {
setIsOpen(false);
};

return (
<Dialog onClose={onClose} open={isOpen} maxWidth="lg">
<DialogHeader onClose={onClose} title={title}>
<FlexSpaceBetween>
<MuiButton
startIcon={<DownloadIcon />}
variant="outlined"
onClick={handleClickExport}
disableElevation
disabled={isDisabled}
>
<I18n t="dashboards.download" />
</MuiButton>
<OptionsBar
totalPage={totalPage}
page={page}
setPage={setPage}
isDisabled={isDisabled}
exportOptions={{
exportWithLabels,
exportWithTable,
toggleExportWithLabels,
toggleExportWithTable,
}}
/>
</FlexSpaceBetween>
</DialogHeader>
<DialogContent>
<LoadingContainer
heading={I18n({
t: isFetching ? 'dashboards.fetchingAllReportsData' : 'dashboards.exportingChartsToPDF',
})}
text={I18n({ t: 'dashboards.pleaseDoNotRefreshTheBrowserOrCloseThisPage' })}
isLoading={isDisabled}
errorMessage={errorMessage}
onReset={onReset}
>
<ExportView
viewType={DASHBOARD_EXPORT_PREVIEW}
viewProps={{
currentPage: page,
isExporting,
isError,
exportOptions: {
exportWithLabels,
exportWithTable,
},
}}
/>
</LoadingContainer>
</DialogContent>
</Dialog>
);
};

DashboardExportModal.propTypes = {
title: PropTypes.string,
totalPage: PropTypes.number,
isOpen: PropTypes.bool.isRequired,
setIsOpen: PropTypes.func.isRequired,
};

DashboardExportModal.defaultProps = {
title: null,
totalPage: 1,
};
Loading

0 comments on commit d76455e

Please sign in to comment.