diff --git a/.gitignore b/.gitignore index cffa7b95..5fbc87bc 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,4 @@ bak/ src/dev .eslintcache /server +deploy/auth.sh diff --git a/deploy/build-and-deploy-app.sh b/deploy/build-and-deploy-app.sh new file mode 100755 index 00000000..2c6b5e5c --- /dev/null +++ b/deploy/build-and-deploy-app.sh @@ -0,0 +1,37 @@ +#!/bin/bash +set -e -u -o pipefail + +script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +zipfile=data-management-app.zip + +debug() { + echo "$@" >&2 +} + +build_app() { + test "${SKIP_BUILD:-}" && return 0 + debug "Build App" + yarn build-webapp +} + +deploy_app() { + local url=$1 auth=$2 + test "${SKIP_DEPLOY_APP:-}" && return 0 + + debug "Post App: $zipfile -> $url" + curl -X POST -u "$auth" -v -F file=@"$zipfile" "$url/api/apps" +} + +build_and_deploy() { + local url=$1 auth=$2 host=${3:-} + + build_app + deploy_app "$url" "$auth" + if test "$host"; then + bash "$script_dir/deploy-scripts.sh" "$host" + fi + + echo "Done" +} + +build_and_deploy "$@" diff --git a/deploy/clone-pro-to-test.sh b/deploy/clone-pro-to-test.sh new file mode 100644 index 00000000..9e09c2a1 --- /dev/null +++ b/deploy/clone-pro-to-test.sh @@ -0,0 +1,15 @@ +#!/bin/bash +set -e -u -o pipefail + +# Actions: +# +# - In PRO: Create docker image and push to Harbor. +# - In TEST: Pull docker from Harbor and start. + +# Requirements: Open vendorlink (spintldhis01, stintldhis01) + +cd "$(dirname "$0")" +source "./lib.sh" + +run spintldhis01 bash push-pro-docker.sh +run stintldhis01 bash deploy-test-from-pro.sh diff --git a/deploy/deploy-scripts.sh b/deploy/deploy-scripts.sh new file mode 100644 index 00000000..fe582675 --- /dev/null +++ b/deploy/deploy-scripts.sh @@ -0,0 +1,26 @@ +#!/bin/bash +# shellcheck disable=SC2029 +set -e -u -o pipefail + +script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) + +debug() { + echo "$@" >&2 +} + +get_current_branch() { + git rev-parse --abbrev-ref HEAD +} + +deploy_scripts() { + local host=$1 + + local current_branch + current_branch=$(get_current_branch) + debug "Deploy branch: $current_branch -> $host" + + rsync -v "$script_dir/deploy-remote-scripts.sh" "$host:" + ssh "$host" "sudo bash deploy-remote-scripts.sh $current_branch" +} + +deploy_scripts "$@" diff --git a/deploy/deploy-test-from-pro.sh b/deploy/deploy-test-from-pro.sh new file mode 100755 index 00000000..1de1e26d --- /dev/null +++ b/deploy/deploy-test-from-pro.sh @@ -0,0 +1,45 @@ +#!/bin/bash +set -e -u -o pipefail + +cd "$(dirname "$0")" +script_dir=$(pwd) +source "./lib.sh" +source "./tasks.sh" + +image_test="docker.eyeseetea.com/samaritans/dhis2-data:40.4.0-sp-ip-test" + +start_from_pro() { + local url=$1 + local image_test_running + + image_test_running=$(d2-docker list | grep RUN | awk '{print $1}' | grep -m1 ip-test) || true + if test "$image_test_running"; then + d2-docker commit "$image_test_running" + d2-docker copy "$image_test_running" "backup/sp-ip-test-$(timestamp)" + d2-docker stop "$image_test_running" + fi + + d2-docker pull "$image_pro" + d2-docker copy "$image_pro" "$image_test" + sudo image=$image_test /usr/local/bin/start-dhis2-test + + wait_for_dhis2_server "$url" +} + +post_clone() { + local url=$1 + + #set_email_password "$url" + change_server_name "$url" "SP Platform - Test" + set_logos "$url" "$script_dir/test-icons" +} + +main() { + local url + url=$(get_url 80) + + start_from_pro "$url" + post_clone "$url" +} + +main "$@" diff --git a/deploy/lib.sh b/deploy/lib.sh index e92b3056..298e1adc 100755 --- a/deploy/lib.sh +++ b/deploy/lib.sh @@ -3,7 +3,6 @@ script_dir="$(dirname "$(readlink -f "${BASH_SOURCE[0]:-$0}")")" source "$script_dir/auth.sh" export image_pro="docker.eyeseetea.com/eyeseetea/dhis2-data:2.36.11.1-sp-ip-pro" -export image_test="docker.eyeseetea.com/samaritans/dhis2-data:2.36.11.1-sp-ip-test" export image_dev="docker.eyeseetea.com/samaritans/dhis2-data:2.36.11.1-sp-ip-dev" export image_training="docker.eyeseetea.com/samaritans/dhis2-data:2.36.11.1-sp-ip-training" @@ -59,6 +58,7 @@ get_user_role_ids() { change_server_name() { local url=$1 title=$2 + debug "Change server name: $title" curl -sS -X POST "$url/api/systemSettings/applicationTitle" \ -d "$title" -H "Content-Type: application/json" @@ -66,6 +66,7 @@ change_server_name() { join_by() { local IFS="$1" + shift echo "$*" } diff --git a/deploy/tasks.sh b/deploy/tasks.sh index 27b8d436..e89852c8 100755 --- a/deploy/tasks.sh +++ b/deploy/tasks.sh @@ -42,7 +42,7 @@ enable_users() { } set_email_password() { - local url=$url + local url=$1 echo "Set email password" curl -sS -H 'Content-Type: text/plain' -u "$auth" \ "$url/api/systemSettings/keyEmailPassword" \ diff --git a/deploy/test-icons/logo_banner.png b/deploy/test-icons/logo_banner.png new file mode 100644 index 00000000..d25955ab Binary files /dev/null and b/deploy/test-icons/logo_banner.png differ diff --git a/deploy/test-icons/logo_front.png b/deploy/test-icons/logo_front.png new file mode 100644 index 00000000..658c1798 Binary files /dev/null and b/deploy/test-icons/logo_front.png differ diff --git a/package.json b/package.json index aa6f2ae0..8d70f811 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "data-management-app", "description": "DHIS2 Data Management App", - "version": "1.7.1", + "version": "2.0.0", "license": "GPL-3.0", "author": "EyeSeeTea team", "homepage": ".", @@ -10,7 +10,7 @@ "url": "git+https://github.com/eyeseetea/project-monitoring-app.git" }, "dependencies": { - "@dhis2/app-runtime": "2.11.1", + "@dhis2/app-runtime": "3.2.1", "@dhis2/d2-i18n": "1.1.1", "@dhis2/d2-i18n-extract": "1.0.8", "@dhis2/d2-i18n-generate": "1.2.0", @@ -51,6 +51,7 @@ "striptags": "3.2.0", "styled-components": "^5.2.1", "styled-jsx": "3.4.1", + "use-debounce": "7.0.0", "word-wrap": "1.2.5", "xlsx": "^0.18.5" }, diff --git a/src/components/dashboard/Dashboard.tsx b/src/components/dashboard/Dashboard.tsx index f87bb8f1..e937d22f 100644 --- a/src/components/dashboard/Dashboard.tsx +++ b/src/components/dashboard/Dashboard.tsx @@ -124,13 +124,16 @@ function autoResizeIframeByContent( iframe: HTMLIFrameElement, setHeight: (height: number) => void ): IntervalId { - const resize = () => { - // Get the first element that has the real height of the full dashboard (and not the forced large value). + function resize() { + // Get element with real height of the dashboard (not the initially forced large value). const document = iframe?.contentWindow?.document; const height = document?.querySelector(".dashboard-scroll-container > div")?.scrollHeight; - if (height && height > 0) setHeight(height); - }; + if (height && height > 1000) { + setHeight(height); + } + } + return window.setInterval(resize, 1000); } diff --git a/src/components/data-entry/data-entry-hooks.ts b/src/components/data-entry/data-entry-hooks.ts index 8aba0a4b..7044264a 100644 --- a/src/components/data-entry/data-entry-hooks.ts +++ b/src/components/data-entry/data-entry-hooks.ts @@ -164,6 +164,12 @@ export interface Options { getOnSaveEvent?: boolean; } +declare global { + interface Window { + jQuery: any; + } +} + /* Function to eval within the iframe to send/receive events to/from the parent page */ function setupDataEntryInterceptors(options: Options = {}) { const iframeWindow = window as unknown as DataEntryWindow; diff --git a/src/index.ts b/src/index.ts index d9a70b0c..a958bcb9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,8 +11,12 @@ import "./utils/lodash-mixins"; import { D2Api } from "./types/d2-api"; import i18n from "./locales"; +import { Config } from "@dhis2/app-service-config/build/types/types"; + import whyDidYouRender from "@welldone-software/why-did-you-render"; +const apiVersion = 40; + async function getBaseUrl(): Promise { if (process.env.NODE_ENV === "development") { const baseUrl = `/dhis2`; @@ -43,7 +47,12 @@ async function main() { const api = new D2Api({ baseUrl, backend: "fetch", timeout: 60 * 1000 }); const userSettings = (await api.get("/userSettings").getData()) as UserSettings; configI18n(userSettings); - const config = { baseUrl, apiVersion: 30 }; + + const config: Config = { + baseUrl: baseUrl, + apiVersion: apiVersion, + systemInfo: d2.system.systemInfo, + }; try { ReactDOM.render( diff --git a/src/models/Dashboard.ts b/src/models/Dashboard.ts index 2921bdb0..1af05985 100644 --- a/src/models/Dashboard.ts +++ b/src/models/Dashboard.ts @@ -146,7 +146,7 @@ export function getChartDashboardItem( return { id: getUid("dashboardItem", chart.id), type: "CHART" as const, - chart: { id: chart.id }, + visualization: { id: chart.id }, ...(dashboardItemAttributes || {}), }; } diff --git a/src/models/ProjectDataSet.ts b/src/models/ProjectDataSet.ts index 959bdbe1..4fa7f979 100644 --- a/src/models/ProjectDataSet.ts +++ b/src/models/ProjectDataSet.ts @@ -223,7 +223,7 @@ export default class ProjectDataSet { .getData(); } - private getAttributeOptionCombo() { + getAttributeOptionCombo() { const categoryOption = this.config.categoryOptions[this.dataSetType]; const aoc = categoryOption.categoryOptionCombos[0]; if (!aoc) throw new Error("Cannot get attribute option combo"); diff --git a/src/models/ProjectDb.ts b/src/models/ProjectDb.ts index 5c7cdbc1..50256bec 100644 --- a/src/models/ProjectDb.ts +++ b/src/models/ProjectDb.ts @@ -297,6 +297,7 @@ export default class ProjectDb { const dataSetTargetMetadata = this.getDataSetMetadata(orgUnit, { name: `${project.name} Target`, + shortName: `${project.name.slice(0, 25)} [${project.id}] Target`, code: "TARGET", attributeValues: dataSetAttributeValues, workflow: { id: config.dataApprovalWorkflows.project.id }, @@ -306,6 +307,7 @@ export default class ProjectDb { const dataSetActualMetadata = this.getDataSetMetadata(orgUnit, { name: `${project.name} Actual`, + shortName: `${project.name.slice(0, 25)} [${project.id}] Actual`, code: "ACTUAL", attributeValues: dataSetAttributeValues, workflow: { id: config.dataApprovalWorkflows.project.id }, diff --git a/src/models/__tests__/data/project-db-metadata.json b/src/models/__tests__/data/project-db-metadata.json index 6d357913..93e60f6e 100644 --- a/src/models/__tests__/data/project-db-metadata.json +++ b/src/models/__tests__/data/project-db-metadata.json @@ -46,7 +46,6 @@ "created": "2020-01-02T12:32:30.449", "lastUpdated": "2020-01-02T14:28:43.045", "name": "Abaco", - "shortName": "Abaco", "id": "GG0k0oNhgS7", "publicAccess": "rw------", "lastUpdatedBy": { @@ -63,14 +62,14 @@ { "id": "eu2XF73JOzl" } - ] + ], + "shortName": "Abaco" }, { "code": "FUNDER_AGRIDIUS", "created": "2020-01-02T11:55:10.244", "lastUpdated": "2020-01-02T14:28:43.050", "name": "Agridius Foundation", - "shortName": "Agridius Foundation", "id": "em8NIwi0KvM", "publicAccess": "rw------", "lastUpdatedBy": { @@ -83,7 +82,8 @@ "attributeValues": [], "translations": [], "userAccesses": [], - "organisationUnits": [] + "organisationUnits": [], + "shortName": "Agridius Foundation" }, { "code": "FUNDER_AC", @@ -158,6 +158,7 @@ "timelyDays": 0, "formType": "DEFAULT", "name": "MyProject Target", + "shortName": "MyProject [WGC0DJ0YSis] Target", "code": "WGC0DJ0YSis_TARGET", "attributeValues": [ { @@ -578,6 +579,7 @@ "timelyDays": 0, "formType": "DEFAULT", "name": "MyProject Actual", + "shortName": "MyProject [WGC0DJ0YSis] Actual", "code": "WGC0DJ0YSis_ACTUAL", "attributeValues": [ { @@ -1078,7 +1080,7 @@ { "id": "ys0CVedHirZ", "type": "CHART", - "chart": { + "visualization": { "id": "uG9C9z46CNK" }, "width": 58, @@ -1089,7 +1091,7 @@ { "id": "KI0C90Ol10x", "type": "CHART", - "chart": { + "visualization": { "id": "qmsj4FqnVPX" }, "width": 29, @@ -1100,7 +1102,7 @@ { "id": "uYY7v977Ubm", "type": "CHART", - "chart": { + "visualization": { "id": "uiyYMLiDaK2" }, "width": 29, @@ -1111,7 +1113,7 @@ { "id": "qUqsDmtiBLF", "type": "CHART", - "chart": { + "visualization": { "id": "u6Sin4Fy1Wt" }, "width": 29, @@ -1122,7 +1124,7 @@ { "id": "mwMpIdPdu8H", "type": "CHART", - "chart": { + "visualization": { "id": "ukewRkZsyCI" }, "width": 29, @@ -1160,7 +1162,7 @@ { "id": "e6UAresDcfM", "type": "CHART", - "chart": { + "visualization": { "id": "GqYJDh6asM8" }, "width": 58, @@ -1171,7 +1173,7 @@ { "id": "CQuufxhy672", "type": "CHART", - "chart": { + "visualization": { "id": "me6p7L8VHXl" }, "width": 58, @@ -1299,7 +1301,7 @@ { "id": "CkcoElgGMuu", "type": "CHART", - "chart": { + "visualization": { "id": "CYYNLjxd96q" }, "width": 58, @@ -1310,7 +1312,7 @@ { "id": "GqqyIiwIxog", "type": "CHART", - "chart": { + "visualization": { "id": "Gi2PegQJhbu" }, "width": 29, @@ -1321,7 +1323,7 @@ { "id": "mCeCHYX6DOG", "type": "CHART", - "chart": { + "visualization": { "id": "Ww2X5s48uQx" }, "width": 29, @@ -1332,7 +1334,7 @@ { "id": "O8EPBVIg4zK", "type": "CHART", - "chart": { + "visualization": { "id": "CMqisHj7sP9" }, "width": 29, @@ -1343,7 +1345,7 @@ { "id": "OI8TXjJJwfS", "type": "CHART", - "chart": { + "visualization": { "id": "WGWGaYtwJzp" }, "width": 29, diff --git a/src/pages/data-approval/DataApproval.tsx b/src/pages/data-approval/DataApproval.tsx index 3f7ef344..87414b8c 100644 --- a/src/pages/data-approval/DataApproval.tsx +++ b/src/pages/data-approval/DataApproval.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useMemo } from "react"; +import React, { useState, useEffect } from "react"; import moment from "moment"; import { useRouteMatch } from "react-router"; import { makeStyles } from "@material-ui/core/styles"; @@ -17,14 +17,7 @@ import { useDialog } from "./data-approval-hooks"; import { DataApprovalMessage } from "./DataApprovalMessage"; import { useGoTo } from "../../router"; import { useHistory } from "react-router-dom"; - -declare global { - interface Window { - jQuery: any; - } -} - -const jQuery = window.jQuery || {}; +import DataApprovalTable from "./DataApprovalTable"; const monthFormat = "YYYYMM"; @@ -34,7 +27,6 @@ type State = { loading: boolean; error?: string; project?: Project; - report?: string; showApproveButton: boolean; showUnapproveButton: boolean; }; @@ -53,7 +45,7 @@ const DataApproval: React.FC = () => { showApproveButton: false, showUnapproveButton: false, }); - const { project, report, error } = state; + const { project, error } = state; const classes = useStyles(); const projectDataSet = React.useMemo(() => { @@ -106,15 +98,11 @@ const DataApproval: React.FC = () => { useEffect(() => loadData(projectId, api, config, setState), [api, config, projectId]); useEffect(() => { - getReport(projectDataSet, projectPeriod, setState); + setApprovalStatus(projectDataSet, projectPeriod, setState); }, [projectDataSet, projectPeriod]); useDebugValuesOnDev(project, setState); - const reportHtml = useMemo(() => { - return { __html: report || "" }; - }, [report]); - const dataApprovalDialog = useDialog(); const setDate = React.useCallback( @@ -139,6 +127,13 @@ const DataApproval: React.FC = () => { [goTo, projectDataSetType, projectId, projectPeriod] ); + const periodStartEnd = React.useMemo(() => { + return { + startDate: moment(projectPeriod, monthFormat).format("YYYY-MM"), + endDate: moment(projectPeriod, monthFormat).add(1, "month").format("YYYY-MM"), + }; + }, [projectPeriod]); + if (!projectPeriod || !projectDataSetType) return null; return ( @@ -172,7 +167,7 @@ const DataApproval: React.FC = () => { {error &&

{error}

} - {report && ( + {projectDataSet && ( { href={api.baseUrl + "/dhis-web-commons/css/light_blue/light_blue.css"} /> -
+ {state.showApproveButton && (