Skip to content

Commit 94f0340

Browse files
committed
Merge remote-tracking branch 'upstream/master'
2 parents a9d0598 + 3228af9 commit 94f0340

File tree

4 files changed

+189
-96
lines changed

4 files changed

+189
-96
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "metadata-synchronization",
33
"description": "Advanced metadata & data synchronization utility",
4-
"version": "2.2.0",
4+
"version": "2.2.1",
55
"license": "GPL-3.0",
66
"author": "EyeSeeTea team",
77
"homepage": ".",

src/data/events/EventsD2ApiRepository.ts

Lines changed: 76 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
} from "../../domain/synchronization/entities/SynchronizationResult";
1111
import { cleanObjectDefault, cleanOrgUnitPaths } from "../../domain/synchronization/utils";
1212
import { DataImportParams } from "../../types/d2";
13-
import { D2Api } from "../../types/d2-api";
13+
import { D2Api, Pager } from "../../types/d2-api";
1414

1515
export class EventsD2ApiRepository implements EventsRepository {
1616
private api: D2Api;
@@ -24,31 +24,91 @@ export class EventsD2ApiRepository implements EventsRepository {
2424
programs: string[] = [],
2525
defaults: string[] = []
2626
): Promise<ProgramEvent[]> {
27-
const { period, orgUnitPaths = [], events = [], allEvents } = params;
28-
const [startDate, endDate] = buildPeriodFromParams(params);
27+
if (params.allEvents) return this.getAllEvents(params, programs, defaults);
28+
else return this.getSpecificEvents(params, programs, defaults);
29+
}
2930

31+
/**
32+
* Design choices and heads-up:
33+
* - The events endpoint does not support multiple values for a given filter
34+
* meaning you cannot query for multiple programs or multiple orgUnits in
35+
* the same API call. Instead you need to query one by one
36+
* - Querying one by one is not performant, instead we query for all events
37+
* available in the instance and manually filter them in this method
38+
* - For big databases querying for all events available in a given instance
39+
* with paging=false makes the instance to eventually go offline
40+
* - Instead of disabling paging we traverse all the events by paginating all
41+
* the available pages so that we can filter them afterwards
42+
*/
43+
private async getAllEvents(
44+
params: DataSynchronizationParams,
45+
programs: string[] = [],
46+
defaults: string[] = []
47+
): Promise<ProgramEvent[]> {
3048
if (programs.length === 0) return [];
3149

32-
const orgUnits = cleanOrgUnitPaths(orgUnitPaths);
50+
const { period, orgUnitPaths = [] } = params;
51+
const [startDate, endDate] = buildPeriodFromParams(params);
3352

53+
const orgUnits = cleanOrgUnitPaths(orgUnitPaths);
3454
const result = [];
3555

36-
for (const program of programs) {
37-
const { events: response } = (await this.api
38-
.get("/events", {
39-
paging: false,
56+
const fetchApi = async (program: string, page: number) => {
57+
return this.api
58+
.get<EventExportResult>("/events", {
59+
pageSize: 250,
60+
totalPages: true,
61+
page,
4062
program,
4163
startDate: period !== "ALL" ? startDate.format("YYYY-MM-DD") : undefined,
4264
endDate: period !== "ALL" ? endDate.format("YYYY-MM-DD") : undefined,
4365
})
44-
.getData()) as { events: (ProgramEvent & { event: string })[] };
66+
.getData();
67+
};
68+
69+
for (const program of programs) {
70+
const { events, pager } = await fetchApi(program, 1);
71+
result.push(...events);
4572

46-
result.push(...response);
73+
for (let page = 2; page <= pager.pageCount; page += 1) {
74+
const { events } = await fetchApi(program, page);
75+
result.push(...events);
76+
}
77+
}
78+
79+
return _(result)
80+
.filter(({ orgUnit }) => orgUnits.includes(orgUnit))
81+
.map(object => ({ ...object, id: object.event }))
82+
.map(object => cleanObjectDefault(object, defaults))
83+
.value();
84+
}
85+
86+
private async getSpecificEvents(
87+
params: DataSynchronizationParams,
88+
programs: string[] = [],
89+
defaults: string[] = []
90+
): Promise<ProgramEvent[]> {
91+
const { orgUnitPaths = [], events: filter = [] } = params;
92+
if (programs.length === 0 || filter.length === 0) return [];
93+
94+
const orgUnits = cleanOrgUnitPaths(orgUnitPaths);
95+
const result = [];
96+
97+
for (const program of programs) {
98+
for (const ids of _.chunk(filter, 300)) {
99+
const { events } = await this.api
100+
.get<EventExportResult>("/events", {
101+
paging: false,
102+
program,
103+
event: ids.join(";"),
104+
})
105+
.getData();
106+
result.push(...events);
107+
}
47108
}
48109

49110
return _(result)
50111
.filter(({ orgUnit }) => orgUnits.includes(orgUnit))
51-
.filter(({ event }) => (allEvents ? true : events.includes(event)))
52112
.map(object => ({ ...object, id: object.event }))
53113
.map(object => cleanObjectDefault(object, defaults))
54114
.value();
@@ -125,3 +185,8 @@ interface EventsPostResponse {
125185
}[];
126186
};
127187
}
188+
189+
interface EventExportResult {
190+
events: Array<ProgramEvent & { event: string }>;
191+
pager: Pager;
192+
}

src/data/metadata/__tests__/integration/sync-events.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ describe("Sync metadata", () => {
9393
remote.get("/dataValueSets", async () => ({ dataValues: [] }));
9494

9595
local.get("/events", async () => ({
96+
pager: { page: 1, pageCount: 1, pageSize: 1, total: 1 },
9697
events: [
9798
{
9899
storedBy: "widp.admin",
@@ -125,6 +126,7 @@ describe("Sync metadata", () => {
125126
}));
126127

127128
remote.get("/events", async () => ({
129+
pager: { page: 1, pageCount: 1, pageSize: 1, total: 1 },
128130
events: [
129131
{
130132
storedBy: "widp.admin",

src/presentation/react/components/sync-wizard/data/EventsSelectionStep.tsx

Lines changed: 110 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { Typography } from "@material-ui/core";
22
import { ObjectsTable, ObjectsTableDetailField, TableColumn, TableState } from "d2-ui-components";
33
import _ from "lodash";
4-
import React, { useEffect, useState } from "react";
4+
import React, { useCallback, useEffect, useMemo, useState } from "react";
55
import { ProgramEvent } from "../../../../../domain/events/entities/ProgramEvent";
66
import { DataElement, Program } from "../../../../../domain/metadata/entities/MetadataEntities";
77
import i18n from "../../../../../locales";
8+
import SyncRule from "../../../../../models/syncRule";
89
import { useAppContext } from "../../../contexts/AppContext";
910
import Dropdown from "../../dropdown/Dropdown";
1011
import { Toggle } from "../../toggle/Toggle";
@@ -20,30 +21,127 @@ type CustomProgram = Program & {
2021

2122
export default function EventsSelectionStep({ syncRule, onChange }: SyncWizardStepProps) {
2223
const { compositionRoot } = useAppContext();
24+
25+
const [memoizedSyncRule] = useState<SyncRule>(syncRule);
2326
const [objects, setObjects] = useState<ProgramEvent[] | undefined>();
2427
const [programs, setPrograms] = useState<CustomProgram[]>([]);
2528
const [programFilter, changeProgramFilter] = useState<string>("");
26-
const [error, setError] = useState();
29+
const [error, setError] = useState<unknown>();
30+
31+
useEffect(() => {
32+
const sync = compositionRoot.sync.events(memoizedSyncRule.toBuilder());
33+
sync.extractMetadata<CustomProgram>().then(({ programs = [] }) => setPrograms(programs));
34+
}, [memoizedSyncRule, compositionRoot]);
2735

2836
useEffect(() => {
37+
if (programs.length === 0) return;
2938
compositionRoot.events
3039
.list(
3140
{
32-
...syncRule.dataParams,
41+
...memoizedSyncRule.dataParams,
3342
allEvents: true,
3443
},
3544
programs.map(({ id }) => id)
3645
)
3746
.then(setObjects)
3847
.catch(setError);
39-
}, [compositionRoot, syncRule, programs]);
48+
}, [compositionRoot, memoizedSyncRule, programs]);
4049

41-
useEffect(() => {
42-
const sync = compositionRoot.sync.events(syncRule.toBuilder());
43-
sync.extractMetadata<CustomProgram>().then(({ programs = [] }) => setPrograms(programs));
44-
}, [syncRule, compositionRoot]);
50+
const handleTableChange = useCallback(
51+
(tableState: TableState<ProgramEvent>) => {
52+
const { selection } = tableState;
53+
onChange(syncRule.updateDataSyncEvents(selection.map(({ id }) => id)));
54+
},
55+
[onChange, syncRule]
56+
);
57+
58+
const updateSyncAll = useCallback(
59+
(value: boolean) => {
60+
onChange(syncRule.updateDataSyncAllEvents(value).updateDataSyncEvents(undefined));
61+
},
62+
[onChange, syncRule]
63+
);
64+
65+
const addToSelection = useCallback(
66+
(ids: string[]) => {
67+
const oldSelection = _.difference(syncRule.dataSyncEvents, ids);
68+
const newSelection = _.difference(ids, syncRule.dataSyncEvents);
69+
70+
onChange(syncRule.updateDataSyncEvents([...oldSelection, ...newSelection]));
71+
},
72+
[onChange, syncRule]
73+
);
74+
75+
const columns: TableColumn<ProgramEvent>[] = useMemo(
76+
() => [
77+
{ name: "id" as const, text: i18n.t("UID"), sortable: true },
78+
{
79+
name: "program" as const,
80+
text: i18n.t("Program"),
81+
sortable: true,
82+
getValue: ({ program }) => _.find(programs, { id: program })?.name ?? program,
83+
},
84+
{ name: "orgUnitName" as const, text: i18n.t("Organisation unit"), sortable: true },
85+
{ name: "eventDate" as const, text: i18n.t("Event date"), sortable: true },
86+
{
87+
name: "lastUpdated" as const,
88+
text: i18n.t("Last updated"),
89+
sortable: true,
90+
hidden: true,
91+
},
92+
{ name: "status" as const, text: i18n.t("Status"), sortable: true },
93+
{ name: "storedBy" as const, text: i18n.t("Stored by"), sortable: true },
94+
],
95+
[programs]
96+
);
97+
98+
const details: ObjectsTableDetailField<ProgramEvent>[] = useMemo(
99+
() => [
100+
{ name: "id" as const, text: i18n.t("UID") },
101+
{
102+
name: "program" as const,
103+
text: i18n.t("Program"),
104+
getValue: ({ program }) => _.find(programs, { id: program })?.name ?? program,
105+
},
106+
{ name: "orgUnitName" as const, text: i18n.t("Organisation unit") },
107+
{ name: "created" as const, text: i18n.t("Created") },
108+
{ name: "lastUpdated" as const, text: i18n.t("Last updated") },
109+
{ name: "eventDate" as const, text: i18n.t("Event date") },
110+
{ name: "dueDate" as const, text: i18n.t("Due date") },
111+
{ name: "status" as const, text: i18n.t("Status") },
112+
{ name: "storedBy" as const, text: i18n.t("Stored by") },
113+
],
114+
[programs]
115+
);
116+
117+
const actions = useMemo(
118+
() => [
119+
{
120+
name: "select",
121+
text: i18n.t("Select"),
122+
primary: true,
123+
multiple: true,
124+
onClick: addToSelection,
125+
isActive: () => false,
126+
},
127+
],
128+
[addToSelection]
129+
);
45130

46-
const buildAdditionalColumns = () => {
131+
const filterComponents = useMemo(
132+
() => (
133+
<Dropdown
134+
key={"program-filter"}
135+
items={programs}
136+
onValueChange={changeProgramFilter}
137+
value={programFilter}
138+
label={i18n.t("Program")}
139+
/>
140+
),
141+
[programFilter, programs]
142+
);
143+
144+
const additionalColumns = useMemo(() => {
47145
const program = _.find(programs, { id: programFilter });
48146
const dataElements = _(program?.programStages ?? [])
49147
.map(({ programStageDataElements }) =>
@@ -61,82 +159,8 @@ export default function EventsSelectionStep({ syncRule, onChange }: SyncWizardSt
61159
return _.find(row.dataValues, { dataElement: id })?.value ?? "-";
62160
},
63161
}));
64-
};
65-
66-
const handleTableChange = (tableState: TableState<ProgramEvent>) => {
67-
const { selection } = tableState;
68-
onChange(syncRule.updateDataSyncEvents(selection.map(({ id }) => id)));
69-
};
70-
71-
const updateSyncAll = (value: boolean) => {
72-
onChange(syncRule.updateDataSyncAllEvents(value).updateDataSyncEvents(undefined));
73-
};
74-
75-
const addToSelection = (ids: string[]) => {
76-
const oldSelection = _.difference(syncRule.dataSyncEvents, ids);
77-
const newSelection = _.difference(ids, syncRule.dataSyncEvents);
78-
79-
onChange(syncRule.updateDataSyncEvents([...oldSelection, ...newSelection]));
80-
};
162+
}, [programFilter, programs]);
81163

82-
const columns: TableColumn<ProgramEvent>[] = [
83-
{ name: "id" as const, text: i18n.t("UID"), sortable: true },
84-
{
85-
name: "program" as const,
86-
text: i18n.t("Program"),
87-
sortable: true,
88-
getValue: ({ program }) => _.find(programs, { id: program })?.name ?? program,
89-
},
90-
{ name: "orgUnitName" as const, text: i18n.t("Organisation unit"), sortable: true },
91-
{ name: "eventDate" as const, text: i18n.t("Event date"), sortable: true },
92-
{
93-
name: "lastUpdated" as const,
94-
text: i18n.t("Last updated"),
95-
sortable: true,
96-
hidden: true,
97-
},
98-
{ name: "status" as const, text: i18n.t("Status"), sortable: true },
99-
{ name: "storedBy" as const, text: i18n.t("Stored by"), sortable: true },
100-
];
101-
102-
const details: ObjectsTableDetailField<ProgramEvent>[] = [
103-
{ name: "id" as const, text: i18n.t("UID") },
104-
{
105-
name: "program" as const,
106-
text: i18n.t("Program"),
107-
getValue: ({ program }) => _.find(programs, { id: program })?.name ?? program,
108-
},
109-
{ name: "orgUnitName" as const, text: i18n.t("Organisation unit") },
110-
{ name: "created" as const, text: i18n.t("Created") },
111-
{ name: "lastUpdated" as const, text: i18n.t("Last updated") },
112-
{ name: "eventDate" as const, text: i18n.t("Event date") },
113-
{ name: "dueDate" as const, text: i18n.t("Due date") },
114-
{ name: "status" as const, text: i18n.t("Status") },
115-
{ name: "storedBy" as const, text: i18n.t("Stored by") },
116-
];
117-
118-
const actions = [
119-
{
120-
name: "select",
121-
text: i18n.t("Select"),
122-
primary: true,
123-
multiple: true,
124-
onClick: addToSelection,
125-
isActive: () => false,
126-
},
127-
];
128-
129-
const filterComponents = (
130-
<Dropdown
131-
key={"program-filter"}
132-
items={programs}
133-
onValueChange={changeProgramFilter}
134-
value={programFilter}
135-
label={i18n.t("Program")}
136-
/>
137-
);
138-
139-
const additionalColumns = buildAdditionalColumns();
140164
const filteredObjects =
141165
objects?.filter(({ program }) => !programFilter || program === programFilter) ?? [];
142166

@@ -149,6 +173,8 @@ export default function EventsSelectionStep({ syncRule, onChange }: SyncWizardSt
149173
);
150174
}
151175

176+
console.log("loading", objects === undefined);
177+
152178
return (
153179
<React.Fragment>
154180
<Toggle

0 commit comments

Comments
 (0)