From 63aebd82b2280c3e0ec2d7745fa6f8e383077d39 Mon Sep 17 00:00:00 2001
From: Biao Li <31789355+billli0@users.noreply.github.com>
Date: Thu, 10 Nov 2022 15:58:10 +1100
Subject: [PATCH 1/5] MAUI-1045 Add unfpa population tile (#4254)
---
...16-UnfpaPopulationTileSet-modifies-data.js | 41 +++++++++++++++++++
packages/web-frontend/src/constants/mapbox.js | 11 +++++
2 files changed, 52 insertions(+)
create mode 100644 packages/database/src/migrations/20221104002716-UnfpaPopulationTileSet-modifies-data.js
diff --git a/packages/database/src/migrations/20221104002716-UnfpaPopulationTileSet-modifies-data.js b/packages/database/src/migrations/20221104002716-UnfpaPopulationTileSet-modifies-data.js
new file mode 100644
index 0000000000..63596744e9
--- /dev/null
+++ b/packages/database/src/migrations/20221104002716-UnfpaPopulationTileSet-modifies-data.js
@@ -0,0 +1,41 @@
+'use strict';
+
+import { updateValues } from '../utilities';
+
+var dbm;
+var type;
+var seed;
+
+/**
+ * We receive the dbmigrate dependency from dbmigrate initially.
+ * This enables us to not have to rely on NODE_PATH.
+ */
+exports.setup = function (options, seedLink) {
+ dbm = options.dbmigrate;
+ type = dbm.dataType;
+ seed = seedLink;
+};
+
+const projectCode = 'unfpa';
+const previousConfig = {
+ permanentRegionLabels: true,
+ projectDashboardHeader: 'Regional',
+};
+const newConfig = {
+ tileSets: 'unfpaPopulation',
+ includeDefaultTileSets: true,
+ permanentRegionLabels: true,
+ projectDashboardHeader: 'Regional',
+};
+
+exports.up = async function (db) {
+ await updateValues(db, 'project', { config: newConfig }, { code: projectCode });
+};
+
+exports.down = async function (db) {
+ await updateValues(db, 'project', { config: previousConfig }, { code: projectCode });
+};
+
+exports._meta = {
+ version: 1,
+};
diff --git a/packages/web-frontend/src/constants/mapbox.js b/packages/web-frontend/src/constants/mapbox.js
index ccc90aa6ef..901733ed9b 100644
--- a/packages/web-frontend/src/constants/mapbox.js
+++ b/packages/web-frontend/src/constants/mapbox.js
@@ -25,6 +25,8 @@ const urls = {
laosEthnicity: makeMapboxStyleUrl({ styleId: 'ckm5nv5rv82j217qka0kylmsu' }),
laosTerrain: makeMapboxStyleUrl({ styleId: 'ckm5o375h43a017qym8ic3sgh' }),
laosPopulation: makeMapboxStyleUrl({ styleId: 'ckm5nolwx0pkt17o7vvgnuya0' }),
+ // UNFPA
+ unfpaPopulation: makeMapboxStyleUrl({ styleId: 'cl5w14no4001m14qyaermcomc' }),
};
const openStreets = key => ({
@@ -156,6 +158,13 @@ const population = key => ({
link: 'https://www.worldpop.org/geodata/listing?id=69',
},
});
+const unfpaPopulation = key => ({
+ key,
+ label: 'Population per 1km',
+ thumbnail:
+ 'https://tupaia.s3-ap-southeast-2.amazonaws.com/uploads/unfpa-population-tile-thumbnail.png',
+ url: urls[key],
+});
export const TILE_SETS = [
openStreets('osm'),
@@ -173,4 +182,6 @@ export const TILE_SETS = [
ethnicity('laosEthnicity'),
terrain('laosTerrain'),
population('laosPopulation'),
+ // UNFPA
+ unfpaPopulation('unfpaPopulation'),
];
From 0f367006bec3fe1d75682c9838092b912906ad25 Mon Sep 17 00:00:00 2001
From: Rohan Port <59544282+rohan-bes@users.noreply.github.com>
Date: Fri, 11 Nov 2022 10:08:05 +1100
Subject: [PATCH 2/5] MAUI-1278: Add ability to add/remove Entity Types
dynamically (#4200)
---
.../src/autocomplete/Autocomplete.js | 2 +
.../admin-panel/src/autocomplete/actions.js | 3 +-
.../src/pages/resources/EntityTypesPage.js | 33 +++++++++++
.../src/pages/resources/ProjectsPage.js | 8 +--
.../admin-panel/src/pages/resources/index.js | 1 +
packages/admin-panel/src/routes.js | 6 ++
.../widgets/InputField/registerInputFields.js | 1 +
.../src/apiV2/GETEntityTypes.js | 21 -------
.../src/apiV2/entityTypes/GetEntityTypes.js | 55 +++++++++++++++++++
.../src/apiV2/entityTypes/index.js | 6 ++
.../getEntityObjectValidator.js | 9 +--
.../ConfigValidator/EntityConfigValidator.js | 6 +-
packages/central-server/src/apiV2/index.js | 4 +-
.../central-server/src/apiV2/postChanges.js | 4 +-
.../constructNewRecordValidationRules.js | 14 +++--
packages/database/src/modelClasses/Entity.js | 11 ++++
.../src/validation/validatorFunctions.js | 5 ++
17 files changed, 145 insertions(+), 44 deletions(-)
create mode 100644 packages/admin-panel/src/pages/resources/EntityTypesPage.js
delete mode 100644 packages/central-server/src/apiV2/GETEntityTypes.js
create mode 100644 packages/central-server/src/apiV2/entityTypes/GetEntityTypes.js
create mode 100644 packages/central-server/src/apiV2/entityTypes/index.js
diff --git a/packages/admin-panel/src/autocomplete/Autocomplete.js b/packages/admin-panel/src/autocomplete/Autocomplete.js
index 0eed5a7b2a..81e87b5715 100644
--- a/packages/admin-panel/src/autocomplete/Autocomplete.js
+++ b/packages/admin-panel/src/autocomplete/Autocomplete.js
@@ -154,6 +154,7 @@ const mapDispatchToProps = (
parentRecord = {},
allowMultipleValues,
baseFilter,
+ pageSize,
},
) => ({
onChangeSelection: (event, newSelection, reason) => {
@@ -188,6 +189,7 @@ const mapDispatchToProps = (
newSearchTerm,
parentRecord,
baseFilter,
+ pageSize,
),
),
onClearState: () => dispatch(clearState(reduxId)),
diff --git a/packages/admin-panel/src/autocomplete/actions.js b/packages/admin-panel/src/autocomplete/actions.js
index eaa07163c6..839a6e7a22 100644
--- a/packages/admin-panel/src/autocomplete/actions.js
+++ b/packages/admin-panel/src/autocomplete/actions.js
@@ -28,6 +28,7 @@ export const changeSearchTerm = (
searchTerm,
parentRecord,
baseFilter = {},
+ pageSize = MAX_AUTOCOMPLETE_RESULTS,
) => async (dispatch, getState, { api }) => {
const fetchId = generateId();
dispatch({
@@ -40,7 +41,7 @@ export const changeSearchTerm = (
const filter = convertSearchTermToFilter({ ...baseFilter, [labelColumn]: searchTerm });
const response = await api.get(makeSubstitutionsInString(endpoint, parentRecord), {
filter: JSON.stringify(filter),
- pageSize: MAX_AUTOCOMPLETE_RESULTS,
+ pageSize,
sort: JSON.stringify([`${labelColumn} ASC`]),
columns: JSON.stringify([labelColumn, valueColumn]),
distinct: true,
diff --git a/packages/admin-panel/src/pages/resources/EntityTypesPage.js b/packages/admin-panel/src/pages/resources/EntityTypesPage.js
new file mode 100644
index 0000000000..4018ad9d67
--- /dev/null
+++ b/packages/admin-panel/src/pages/resources/EntityTypesPage.js
@@ -0,0 +1,33 @@
+/**
+ * Tupaia MediTrak
+ * Copyright (c) 2017 Beyond Essential Systems Pty Ltd
+ */
+
+import React from 'react';
+import PropTypes from 'prop-types';
+import { ResourcePage } from './ResourcePage';
+
+const ENTITY_TYPES_ENDPOINT = 'entityTypes';
+
+export const ENTITY_TYPES_COLUMNS = [
+ { source: 'id', show: false },
+ {
+ Header: 'Type',
+ source: 'type',
+ filterable: false,
+ sortable: false,
+ },
+];
+
+export const EntityTypesPage = ({ getHeaderEl }) => (
+
+);
+
+EntityTypesPage.propTypes = {
+ getHeaderEl: PropTypes.func.isRequired,
+};
diff --git a/packages/admin-panel/src/pages/resources/ProjectsPage.js b/packages/admin-panel/src/pages/resources/ProjectsPage.js
index eca65b5c3a..e776288aea 100644
--- a/packages/admin-panel/src/pages/resources/ProjectsPage.js
+++ b/packages/admin-panel/src/pages/resources/ProjectsPage.js
@@ -102,14 +102,14 @@ const NEW_PROJECT_COLUMNS = [
},
{
Header: 'Canonical Types (leave blank for default)',
- source: 'entityType',
+ source: 'entityTypes',
Filter: ArrayFilter,
Cell: ({ value }) => prettyArray(value),
editConfig: {
optionsEndpoint: 'entityTypes',
- optionLabelKey: 'entityType',
- optionValueKey: 'entityType',
- sourceKey: 'entityTypes',
+ optionLabelKey: 'type',
+ optionValueKey: 'type',
+ pageSize: 1000, // entityTypes endpoint doesn't support filtering, so fetch all values
allowMultipleValues: true,
},
},
diff --git a/packages/admin-panel/src/pages/resources/index.js b/packages/admin-panel/src/pages/resources/index.js
index 0647afbd2b..fc786ed29d 100644
--- a/packages/admin-panel/src/pages/resources/index.js
+++ b/packages/admin-panel/src/pages/resources/index.js
@@ -5,6 +5,7 @@
export { CountriesPage } from './CountriesPage';
export { EntitiesPage } from './EntitiesPage';
+export { EntityTypesPage } from './EntityTypesPage';
export { PermissionsPage } from './PermissionsPage';
export { PermissionGroupsPage } from './PermissionGroupsPage';
export {
diff --git a/packages/admin-panel/src/routes.js b/packages/admin-panel/src/routes.js
index c400cbec89..e21dccb7e7 100644
--- a/packages/admin-panel/src/routes.js
+++ b/packages/admin-panel/src/routes.js
@@ -9,6 +9,7 @@ import { StrivePage } from './pages/StrivePage';
import {
CountriesPage,
EntitiesPage,
+ EntityTypesPage,
OptionSetsPage,
PermissionGroupsPage,
PermissionsPage,
@@ -183,6 +184,11 @@ export const ROUTES = [
to: '/countries',
component: CountriesPage,
},
+ {
+ label: 'Entity Types',
+ to: '/entityTypes',
+ component: EntityTypesPage,
+ },
],
},
{
diff --git a/packages/admin-panel/src/widgets/InputField/registerInputFields.js b/packages/admin-panel/src/widgets/InputField/registerInputFields.js
index 2fdd9c0474..c65bae13a9 100644
--- a/packages/admin-panel/src/widgets/InputField/registerInputFields.js
+++ b/packages/admin-panel/src/widgets/InputField/registerInputFields.js
@@ -55,6 +55,7 @@ export const registerInputFields = () => {
allowMultipleValues={props.allowMultipleValues}
parentRecord={props.parentRecord}
baseFilter={props.baseFilter}
+ pageSize={props.pageSize}
/>
));
registerInputField('json', props => (
diff --git a/packages/central-server/src/apiV2/GETEntityTypes.js b/packages/central-server/src/apiV2/GETEntityTypes.js
deleted file mode 100644
index 9c4d3a29ea..0000000000
--- a/packages/central-server/src/apiV2/GETEntityTypes.js
+++ /dev/null
@@ -1,21 +0,0 @@
-/**
- * Tupaia
- * Copyright (c) 2017 - 2020 Beyond Essential Systems Pty Ltd
- */
-
-import { respond } from '@tupaia/utils';
-
-export const GETEntityTypes = async (req, res, next) => {
- const { models } = req;
- try {
- const { types } = await models.entity;
- const entityTypes = Object.values(types)
- .filter(value => value !== 'world')
- .map(value => {
- return { entityType: value };
- });
- respond(res, entityTypes);
- } catch (error) {
- next(error);
- }
-};
diff --git a/packages/central-server/src/apiV2/entityTypes/GetEntityTypes.js b/packages/central-server/src/apiV2/entityTypes/GetEntityTypes.js
new file mode 100644
index 0000000000..2a68aa15c2
--- /dev/null
+++ b/packages/central-server/src/apiV2/entityTypes/GetEntityTypes.js
@@ -0,0 +1,55 @@
+/**
+ * Tupaia
+ * Copyright (c) 2017 - 2022 Beyond Essential Systems Pty Ltd
+ */
+
+import { allowNoPermissions } from '../../permissions';
+import { GETHandler } from '../GETHandler';
+
+/**
+ * Custom implementation required for this route as there is no corresponding DatabaseModel for EntityType
+ * (it's an enum not a table)
+ */
+export class GetEntityTypes extends GETHandler {
+ async assertUserHasAccess() {
+ await this.assertPermissions(allowNoPermissions);
+ }
+
+ async getEntityTypes() {
+ return this.models.entity.getEntityTypes();
+ }
+
+ async getDbQueryOptions() {
+ const { limit, page } = this.getPaginationParameters();
+ const offset = limit * page;
+
+ return { limit, offset };
+ }
+
+ async findSingleRecord(recordId) {
+ const entityTypes = await this.getEntityTypes();
+ const entityType = entityTypes.find(type => type === recordId);
+ if (!entityType) {
+ return undefined;
+ }
+
+ return { id: entityType, type: entityType };
+ }
+
+ async findRecords(criteria, options) {
+ const { limit, offset } = options;
+ const entityTypes = await this.getEntityTypes();
+ if (offset) {
+ entityTypes.splice(0, offset);
+ }
+ if (limit) {
+ entityTypes.splice(limit);
+ }
+ return entityTypes.map(type => ({ id: type, type }));
+ }
+
+ async countRecords() {
+ const entityTypes = await this.getEntityTypes();
+ return entityTypes.length;
+ }
+}
diff --git a/packages/central-server/src/apiV2/entityTypes/index.js b/packages/central-server/src/apiV2/entityTypes/index.js
new file mode 100644
index 0000000000..b1d056f886
--- /dev/null
+++ b/packages/central-server/src/apiV2/entityTypes/index.js
@@ -0,0 +1,6 @@
+/**
+ * Tupaia
+ * Copyright (c) 2017 - 2022 Beyond Essential Systems Pty Ltd
+ */
+
+export { GetEntityTypes } from './GetEntityTypes';
diff --git a/packages/central-server/src/apiV2/import/importEntities/getEntityObjectValidator.js b/packages/central-server/src/apiV2/import/importEntities/getEntityObjectValidator.js
index 1a1c15bd88..b961c6859b 100644
--- a/packages/central-server/src/apiV2/import/importEntities/getEntityObjectValidator.js
+++ b/packages/central-server/src/apiV2/import/importEntities/getEntityObjectValidator.js
@@ -9,6 +9,7 @@ import {
constructIsEmptyOr,
constructIsOneOf,
isPlainObject,
+ constructIsValidEntityType,
} from '@tupaia/utils';
const constructEntityFieldValidators = models => ({
@@ -17,13 +18,7 @@ const constructEntityFieldValidators = models => ({
sub_district: [],
code: [hasContent],
name: [hasContent],
- entity_type: [
- hasContent,
- cellValue => {
- const checkIsOneOf = constructIsOneOf(Object.values(models.entity.types));
- checkIsOneOf(cellValue);
- },
- ],
+ entity_type: [hasContent, constructIsValidEntityType(models.entity)],
attributes: [constructIsEmptyOr(isPlainObject)],
data_service_entity: [constructIsEmptyOr(isPlainObject)],
});
diff --git a/packages/central-server/src/apiV2/import/importSurveys/Validator/ConfigValidator/EntityConfigValidator.js b/packages/central-server/src/apiV2/import/importSurveys/Validator/ConfigValidator/EntityConfigValidator.js
index bad3c6b47a..4373588536 100644
--- a/packages/central-server/src/apiV2/import/importSurveys/Validator/ConfigValidator/EntityConfigValidator.js
+++ b/packages/central-server/src/apiV2/import/importSurveys/Validator/ConfigValidator/EntityConfigValidator.js
@@ -3,8 +3,8 @@
* Copyright (c) 2019 Beyond Essential Systems Pty Ltd
*/
-import { constructIsNotPresentOr, hasContent } from '@tupaia/utils';
-import { constructListItemsAreOneOf, validateIsYesOrNo } from '../../validatorFunctions';
+import { constructIsNotPresentOr, constructIsValidEntityType, hasContent } from '@tupaia/utils';
+import { validateIsYesOrNo } from '../../validatorFunctions';
import { ANSWER_TYPES } from '../../../../../database/models/Answer';
import { isEmpty, isYes } from '../../utilities';
import { JsonFieldValidator } from '../JsonFieldValidator';
@@ -45,7 +45,7 @@ export class EntityConfigValidator extends JsonFieldValidator {
);
return {
- type: [hasContent, constructListItemsAreOneOf(Object.values(this.models.entity.types))],
+ type: [hasContent, constructIsValidEntityType(this.models.entity)],
createNew: [constructIsNotPresentOr(validateIsYesOrNo)],
code: [hasContentIfCanCreateNew, constructIsNotPresentOr(pointsToAnotherQuestion)],
name: [hasContentIfCanCreateNew, constructIsNotPresentOr(pointsToAnotherQuestion)],
diff --git a/packages/central-server/src/apiV2/index.js b/packages/central-server/src/apiV2/index.js
index dbd53ceb6a..fdafa1b5bf 100644
--- a/packages/central-server/src/apiV2/index.js
+++ b/packages/central-server/src/apiV2/index.js
@@ -31,7 +31,6 @@ import { GETDisasters } from './GETDisasters';
import { GETDataElements, EditDataElements, DeleteDataElements } from './dataElements';
import { GETDataGroups, EditDataGroups, DeleteDataGroups } from './dataGroups';
import { GETDataTables } from './dataTables';
-import { GETEntityTypes } from './GETEntityTypes';
import { GETFeedItems } from './GETFeedItems';
import { GETGeographicalAreas } from './GETGeographicalAreas';
import { GETSurveyGroups } from './GETSurveyGroups';
@@ -90,6 +89,7 @@ import {
GETUserEntityPermissions,
} from './userEntityPermissions';
import { EditEntity, GETEntities, DeleteEntity } from './entities';
+import { GetEntityTypes } from './entityTypes';
import { EditAccessRequests, GETAccessRequests } from './accessRequests';
import { postChanges } from './postChanges';
import { changePassword } from './changePassword';
@@ -190,7 +190,7 @@ apiV2.get('/mapOverlayGroupRelations/:recordId?', useRouteHandler(GETMapOverlayG
apiV2.get('/surveys/:recordId?', useRouteHandler(GETSurveys));
apiV2.get('/countries/:parentRecordId/surveys', useRouteHandler(GETSurveys));
apiV2.get('/countries/:parentRecordId/entities', useRouteHandler(GETEntities));
-apiV2.get('/entityTypes', allowAnyone(GETEntityTypes));
+apiV2.get('/entityTypes/:recordId?', useRouteHandler(GetEntityTypes));
apiV2.get('/surveyGroups/:recordId?', useRouteHandler(GETSurveyGroups));
apiV2.get('/surveyResponses/:parentRecordId/answers', useRouteHandler(GETAnswers));
apiV2.get('/surveyResponses/:recordId?', useRouteHandler(GETSurveyResponses));
diff --git a/packages/central-server/src/apiV2/postChanges.js b/packages/central-server/src/apiV2/postChanges.js
index e619ae1307..c3f70a8be2 100644
--- a/packages/central-server/src/apiV2/postChanges.js
+++ b/packages/central-server/src/apiV2/postChanges.js
@@ -16,8 +16,8 @@ import {
constructEveryItem,
takesIdForm,
takesDateForm,
- constructIsOneOf,
isNumber,
+ constructIsValidEntityType,
} from '@tupaia/utils';
import { updateOrCreateSurveyResponse, addSurveyImage } from '../dataAccessors';
import { assertCanSubmitSurveyResponses } from './import/importSurveyResponses/assertCanImportSurveyResponses';
@@ -216,7 +216,7 @@ const constructEntitiesCreatedValidators = models => ({
code: [hasContent],
parent_id: [takesIdForm],
name: [hasContent],
- type: [constructIsOneOf(Object.values(models.entity.types))],
+ type: [constructIsValidEntityType(models.entity)],
country_code: [hasContent],
});
diff --git a/packages/central-server/src/apiV2/utilities/constructNewRecordValidationRules.js b/packages/central-server/src/apiV2/utilities/constructNewRecordValidationRules.js
index 950d0a31ce..88ab954456 100644
--- a/packages/central-server/src/apiV2/utilities/constructNewRecordValidationRules.js
+++ b/packages/central-server/src/apiV2/utilities/constructNewRecordValidationRules.js
@@ -15,11 +15,11 @@ import {
isPlainObject,
constructIsEmptyOr,
constructIsOneOf,
- constructIsSubSetOf,
isValidPassword,
isNumber,
ValidationError,
constructRecordExistsWithCode,
+ constructIsValidEntityType,
} from '@tupaia/utils';
import { DATA_SOURCE_SERVICE_TYPES } from '../../database/models/DataElement';
@@ -128,7 +128,12 @@ export const constructForSingle = (models, recordType) => {
return {
dashboard_id: [constructRecordExistsWithId(models.dashboard)],
child_id: [constructRecordExistsWithId(models.dashboardItem)],
- entity_types: [constructIsSubSetOf(Object.values(models.entity.types))],
+ entity_types: [
+ async entityTypes => {
+ const entityTypeValidator = constructIsValidEntityType(models.entity);
+ await Promise.all(entityTypes.map(entityTypeValidator));
+ },
+ ],
permission_groups: [
async permissionGroupNames => {
const permissionGroups = await models.permissionGroup.find({
@@ -255,11 +260,12 @@ export const constructForSingle = (models, recordType) => {
logo_url: [isAString],
entityTypes: [
async selectedEntityTypes => {
+ console.log(selectedEntityTypes);
if (!selectedEntityTypes) {
return true;
}
- const entityDataTypes = await models.entity.types;
- const filteredEntityTypes = Object.values(entityDataTypes).filter(type =>
+ const entityTypes = await models.entity.getEntityTypes();
+ const filteredEntityTypes = entityTypes.filter(type =>
selectedEntityTypes.includes(type),
);
if (selectedEntityTypes.length !== filteredEntityTypes.length) {
diff --git a/packages/database/src/modelClasses/Entity.js b/packages/database/src/modelClasses/Entity.js
index 986d3aea9a..fc6db8cf03 100644
--- a/packages/database/src/modelClasses/Entity.js
+++ b/packages/database/src/modelClasses/Entity.js
@@ -10,6 +10,10 @@ import { DatabaseType } from '../DatabaseType';
import { TYPES } from '../types';
import { QUERY_CONJUNCTIONS } from '../TupaiaDatabase';
+// NOTE: These hard coded entity types are now a legacy pattern
+// Users can now create their own entity types
+// The up-to-date list of entity types can be found by calling
+// entityModel.getEntityTypes()
const CASE = 'case';
const CASE_CONTACT = 'case_contact';
const COUNTRY = 'country';
@@ -493,4 +497,11 @@ export class EntityModel extends MaterializedViewLogDatabaseModel {
return level;
}
+
+ async getEntityTypes() {
+ const entityTypes = await this.database.executeSql(
+ `SELECT UNNEST(enum_range(null::entity_type)) as type;`,
+ );
+ return entityTypes.map(({ type }) => type);
+ }
}
diff --git a/packages/utils/src/validation/validatorFunctions.js b/packages/utils/src/validation/validatorFunctions.js
index 747947235e..a064fd6c36 100644
--- a/packages/utils/src/validation/validatorFunctions.js
+++ b/packages/utils/src/validation/validatorFunctions.js
@@ -323,3 +323,8 @@ export const constructThisOrThatHasContent = otherFieldKey => (value, object) =>
}
return true;
};
+
+export const constructIsValidEntityType = entityModel => async type => {
+ const isOneOfEntityTypesValidator = constructIsOneOf(await entityModel.getEntityTypes());
+ return isOneOfEntityTypesValidator(type);
+};
From 01b332c5d3345247226b9eaaf90c7f37910d25e5 Mon Sep 17 00:00:00 2001
From: Biao Li <31789355+billli0@users.noreply.github.com>
Date: Tue, 15 Nov 2022 09:14:54 +1100
Subject: [PATCH 3/5] MAUI-1228 Map overlay look deeper for polygon data
(#4255)
---
...21119-AddFacilitiesPenfaa-modifies-data.js | 137 ++++++++++++++++++
.../InteractivePolygonLayer.js | 44 +++++-
2 files changed, 173 insertions(+), 8 deletions(-)
create mode 100644 packages/database/src/migrations/20221110021119-AddFacilitiesPenfaa-modifies-data.js
diff --git a/packages/database/src/migrations/20221110021119-AddFacilitiesPenfaa-modifies-data.js b/packages/database/src/migrations/20221110021119-AddFacilitiesPenfaa-modifies-data.js
new file mode 100644
index 0000000000..fdb073b53e
--- /dev/null
+++ b/packages/database/src/migrations/20221110021119-AddFacilitiesPenfaa-modifies-data.js
@@ -0,0 +1,137 @@
+'use strict';
+
+const { generateId, nameToId } = require('../utilities');
+
+var dbm;
+var type;
+var seed;
+
+/**
+ * We receive the dbmigrate dependency from dbmigrate initially.
+ * This enables us to not have to rely on NODE_PATH.
+ */
+exports.setup = function (options, seedLink) {
+ dbm = options.dbmigrate;
+ type = dbm.dataType;
+ seed = seedLink;
+};
+
+// Current hierarchy
+// country
+// |- district
+// |- sub_district
+// |- ...
+
+// New hierarchy
+// country
+// |- district
+// |- facility <-- add facilities to alt. hierarchy. All facilities already exist.
+// |- sub_district
+// |- ...
+
+const HIERARCHY_CODE = 'penfaa_samoa';
+
+// a subset of facilities in Samoa
+const FACILITIES = [
+ // parent_code code
+ ['WS_Upolu', 'WS_001'],
+ ['WS_Savaii', 'WS_002'],
+ ['WS_Upolu', 'WS_003'],
+ ['WS_Upolu', 'WS_004'],
+ ['WS_Upolu', 'WS_005'],
+ ['WS_Savaii', 'WS_006'],
+ ['WS_Upolu', 'WS_007'],
+ ['WS_Upolu', 'WS_008'],
+ ['WS_Savaii', 'WS_009'],
+ ['WS_Savaii', 'WS_011'],
+ ['WS_Savaii', 'WS_012'],
+ ['WS_Upolu', 'WS_014'],
+];
+
+const SUB_DISTRICTS = [
+ // parent_code code
+ ['WS_004', 'WS_sd01'],
+ ['WS_004', 'WS_sd02'],
+ ['WS_004', 'WS_sd03'],
+ ['WS_004', 'WS_sd04'],
+ ['WS_001', 'WS_sd05'],
+ ['WS_011', 'WS_sd06'],
+ ['WS_003', 'WS_sd07'],
+ ['WS_003', 'WS_sd08'],
+ ['WS_005', 'WS_sd09'],
+ ['WS_005', 'WS_sd10'],
+ ['WS_006', 'WS_sd11'],
+ ['WS_006', 'WS_sd12'],
+ ['WS_006', 'WS_sd13'],
+ ['WS_006', 'WS_sd14'],
+ ['WS_006', 'WS_sd15'],
+ ['WS_007', 'WS_sd16'],
+ ['WS_007', 'WS_sd17'],
+ ['WS_011', 'WS_sd18'],
+ ['WS_014', 'WS_sd19'],
+ ['WS_014', 'WS_sd20'],
+ ['WS_014', 'WS_sd21'],
+ ['WS_014', 'WS_sd22'],
+ ['WS_001', 'WS_sd23'],
+ ['WS_009', 'WS_sd24'],
+ ['WS_009', 'WS_sd25'],
+ ['WS_009', 'WS_sd26'],
+ ['WS_009', 'WS_sd27'],
+ ['WS_009', 'WS_sd28'],
+ ['WS_008', 'WS_sd29'],
+ ['WS_003', 'WS_sd30'],
+ ['WS_003', 'WS_sd31'],
+ ['WS_002', 'WS_sd32'],
+ ['WS_012', 'WS_sd33'],
+ ['WS_012', 'WS_sd34'],
+ ['WS_008', 'WS_sd35'],
+ ['WS_008', 'WS_sd36'],
+ ['WS_004', 'WS_sd37'],
+ ['WS_004', 'WS_sd38'],
+ ['WS_004', 'WS_sd39'],
+ ['WS_004', 'WS_sd40'],
+ ['WS_002', 'WS_sd41'],
+ ['WS_002', 'WS_sd42'],
+ ['WS_012', 'WS_sd43'],
+ ['WS_008', 'WS_sd44'],
+ ['WS_005', 'WS_sd45'],
+ ['WS_014', 'WS_sd46'],
+ ['WS_014', 'WS_sd47'],
+ ['WS_014', 'WS_sd48'],
+ ['WS_014', 'WS_sd49'],
+ ['WS_011', 'WS_sd50'],
+ ['WS_011', 'WS_sd51'],
+];
+
+exports.up = async function (db) {
+ const entityHierarchyId = await nameToId(db, 'entity_hierarchy', HIERARCHY_CODE);
+
+ for (const [parentSubDistrictCode, facilityCode] of FACILITIES) {
+ await db.runSql(`
+ INSERT INTO entity_relation (id, parent_id, child_id, entity_hierarchy_id)
+ VALUES (
+ '${generateId()}',
+ (SELECT id FROM entity WHERE code = '${parentSubDistrictCode}'),
+ (SELECT id FROM entity WHERE code = '${facilityCode}'),
+ '${entityHierarchyId}'
+ )
+ `);
+ }
+
+ for (const [parentFacilityCode, subDistrictCode] of SUB_DISTRICTS) {
+ await db.runSql(`
+ UPDATE entity_relation
+ SET parent_id = (SELECT id FROM entity WHERE code = '${parentFacilityCode}')
+ WHERE child_id = (SELECT id FROM entity WHERE code = '${subDistrictCode}')
+ AND entity_hierarchy_id = '${entityHierarchyId}'
+ `);
+ }
+};
+
+exports.down = async function (db) {
+ return null;
+};
+
+exports._meta = {
+ version: 1,
+};
diff --git a/packages/web-frontend/src/containers/Map/DataVisualsLayer/InteractivePolygonLayer.js b/packages/web-frontend/src/containers/Map/DataVisualsLayer/InteractivePolygonLayer.js
index bbc973660c..8431950a81 100644
--- a/packages/web-frontend/src/containers/Map/DataVisualsLayer/InteractivePolygonLayer.js
+++ b/packages/web-frontend/src/containers/Map/DataVisualsLayer/InteractivePolygonLayer.js
@@ -6,6 +6,7 @@
import React from 'react';
import { connect } from 'react-redux';
+import { omitBy } from 'lodash';
import PropTypes from 'prop-types';
import { InteractivePolygon } from '@tupaia/ui-components';
@@ -19,6 +20,7 @@ import {
selectOrgUnitChildren,
selectOrgUnitSiblings,
} from '../../../selectors';
+import { selectCountryHierarchy } from '../../../selectors/orgUnitSelectors';
const InteractivePolygonLayerComponent = props => {
const {
@@ -101,6 +103,8 @@ const mapStateToProps = (state, ownProps) => {
const currentChildren =
selectOrgUnitChildren(state, currentOrganisationUnit.organisationUnitCode) || [];
const permanentLabels = selectAreRegionLabelsPermanent(state);
+ const country = selectCountryHierarchy(state, currentOrganisationUnit.organisationUnitCode);
+
// orginal data
// If the org unit's grandchildren are polygons and have a measure, display grandchildren
// rather than children
@@ -110,14 +114,38 @@ const mapStateToProps = (state, ownProps) => {
if (selectHasPolygonMeasure(state)) {
measureOrgUnits = selectMeasuresWithDisplayInfo(state, displayedMapOverlayCodes);
const measureOrgUnitCodes = measureOrgUnits.map(orgUnit => orgUnit.organisationUnitCode);
- const grandchildren = currentChildren
- .map(area => selectOrgUnitChildren(state, area.organisationUnitCode))
- .reduce((acc, val) => acc.concat(val), []); // equivelent to .flat(), for IE
-
- const hasShadedGrandchildren =
- grandchildren &&
- grandchildren.some(child => measureOrgUnitCodes.includes(child.organisationUnitCode));
- if (hasShadedGrandchildren) displayedChildren = grandchildren;
+
+ const getDisplayedDescendants = (parents, restOfOrgUnits = {}) => {
+ if (!Array.isArray(parents) || parents.length === 0) return parents;
+ const parentCodes = parents.map(area => area.organisationUnitCode);
+ const children = Object.values(restOfOrgUnits).filter(orgUnit =>
+ parentCodes.includes(orgUnit.parent),
+ );
+
+ if (children.length === 0) return null;
+
+ // if this is the measure layer return it
+ const hasShadedPolygonChildren =
+ children &&
+ children.some(
+ child =>
+ organisationUnitIsArea(child) &&
+ measureOrgUnitCodes.includes(child.organisationUnitCode),
+ );
+ if (hasShadedPolygonChildren) return children;
+
+ // otherwise look deeper
+ return getDisplayedDescendants(
+ children,
+ omitBy(restOfOrgUnits, orgUnit => orgUnit.type === parents[0].type),
+ );
+ };
+
+ const displayedDescendants = getDisplayedDescendants(
+ currentChildren,
+ omitBy(country, orgUnit => orgUnit.type === currentOrganisationUnit.type),
+ );
+ if (displayedDescendants) displayedChildren = displayedDescendants;
}
const getChildren = organisationUnitCode => selectOrgUnitChildren(state, organisationUnitCode);
From 994fda9a2fd47ad43addfaf2e484dcf5d9dc691c Mon Sep 17 00:00:00 2001
From: Igor
Date: Tue, 15 Nov 2022 16:24:25 +1100
Subject: [PATCH 4/5] RN-710 Add superset instances (#4263)
* Remove insecure option for superset api
* Replace needle with node-fetch
* Add superset instances
---
.../src/services/superset/getSupersetApi.js | 4 +-
...0217-AddSupersetInstances-modifies-data.js | 57 ++++++++++++++
packages/superset-api/package.json | 2 +-
packages/superset-api/src/SupersetApi.ts | 76 ++++++++-----------
yarn.lock | 17 +----
5 files changed, 93 insertions(+), 63 deletions(-)
create mode 100644 packages/database/src/migrations/20221111050217-AddSupersetInstances-modifies-data.js
diff --git a/packages/data-broker/src/services/superset/getSupersetApi.js b/packages/data-broker/src/services/superset/getSupersetApi.js
index b5f036f4c1..d6e508a256 100644
--- a/packages/data-broker/src/services/superset/getSupersetApi.js
+++ b/packages/data-broker/src/services/superset/getSupersetApi.js
@@ -14,10 +14,10 @@ const instances = {};
*/
export const getSupersetApiInstance = async (models, supersetInstance) => {
const { code: serverName, config } = supersetInstance;
- const { baseUrl, insecure } = config;
+ const { baseUrl } = config;
if (!instances[serverName]) {
- instances[serverName] = new SupersetApi(serverName, baseUrl, insecure);
+ instances[serverName] = new SupersetApi(serverName, baseUrl);
}
return instances[serverName];
diff --git a/packages/database/src/migrations/20221111050217-AddSupersetInstances-modifies-data.js b/packages/database/src/migrations/20221111050217-AddSupersetInstances-modifies-data.js
new file mode 100644
index 0000000000..dbeab26ec1
--- /dev/null
+++ b/packages/database/src/migrations/20221111050217-AddSupersetInstances-modifies-data.js
@@ -0,0 +1,57 @@
+'use strict';
+
+const { generateId, insertObject } = require('../utilities');
+
+var dbm;
+var type;
+var seed;
+
+/**
+ * We receive the dbmigrate dependency from dbmigrate initially.
+ * This enables us to not have to rely on NODE_PATH.
+ */
+exports.setup = function (options, seedLink) {
+ dbm = options.dbmigrate;
+ type = dbm.dataType;
+ seed = seedLink;
+};
+
+const SUPERSET_INSTANCES = [
+ {
+ id: generateId(),
+ code: 'msupply-kiribati-vax',
+ config: {
+ baseUrl: 'https://superset-kiribati-vax.msupply.org:8088',
+ },
+ },
+ {
+ id: generateId(),
+ code: 'msupply-samoa',
+ config: {
+ baseUrl: 'https://superset-samoa.msupply.org:8088',
+ },
+ },
+ {
+ id: generateId(),
+ code: 'msupply-tonga-vax',
+ config: {
+ baseUrl: 'https://superset-tonga-vax.msupply.org:8088',
+ },
+ },
+];
+
+exports.up = async function (db) {
+ for (const instance of SUPERSET_INSTANCES) {
+ await insertObject(db, 'superset_instance', instance);
+ }
+};
+
+exports.down = async function (db) {
+ for (const instance of SUPERSET_INSTANCES) {
+ await db.runSql(`DELETE FROM superset_instance WHERE code = '${instance.code}'`);
+ }
+};
+
+exports._meta = {
+ version: 1,
+};
diff --git a/packages/superset-api/package.json b/packages/superset-api/package.json
index cc9489d9fb..d4a6b8669b 100644
--- a/packages/superset-api/package.json
+++ b/packages/superset-api/package.json
@@ -20,7 +20,7 @@
"dependencies": {
"@tupaia/utils": "1.0.0",
"https-proxy-agent": "^5.0.1",
- "needle": "^3.1.0",
+ "node-fetch": "^1.7.3",
"winston": "^3.3.3"
},
"devDependencies": {
diff --git a/packages/superset-api/src/SupersetApi.ts b/packages/superset-api/src/SupersetApi.ts
index af2f08efef..182e26e2c9 100644
--- a/packages/superset-api/src/SupersetApi.ts
+++ b/packages/superset-api/src/SupersetApi.ts
@@ -9,29 +9,21 @@ import {
} from './types';
import winston from 'winston';
import { HttpsProxyAgent } from 'https-proxy-agent';
-import needle from 'needle';
-import type {
- NeedleHttpVerbs,
- BodyData as NeedleBodyData,
- NeedleOptions,
- NeedleResponse,
-} from 'needle';
+import fetch, { RequestInit, Response } from 'node-fetch';
const MAX_RETRIES = 1;
export class SupersetApi {
protected serverName: string;
protected baseUrl: string;
- protected insecure: boolean;
protected accessToken: string | null = null;
protected proxyAgent?: HttpsProxyAgent;
- public constructor(serverName: string, baseUrl: string, insecure: boolean = false) {
+ public constructor(serverName: string, baseUrl: string) {
if (!serverName) throw new Error('Argument serverName required');
if (!baseUrl) throw new Error('Argument baseUrl required');
this.serverName = serverName;
this.baseUrl = baseUrl;
- this.insecure = insecure;
const proxyUrl = this.getServerVariable('SUPERSET_API_PROXY_URL');
if (proxyUrl) {
winston.info(`Superset using proxy`);
@@ -53,27 +45,33 @@ export class SupersetApi {
return this.fetch(url, numRetries + 1);
}
- const fetchConfig: any = {
+ const options: RequestInit = {
+ method: 'get',
headers: {
Authorization: `Bearer ${this.accessToken}`,
'Content-Type': 'application/json',
},
};
- const result = await this.apiRequest('get', url, undefined, fetchConfig);
+ const result = await this.apiRequest(url, options);
- if (result.statusCode !== 200) {
- if (result.statusCode === 422 || result.statusCode === 401) {
+ if (result.status !== 200) {
+ if (result.status === 422 || result.status === 401) {
winston.info(`Superset Auth error, response: ${result.body}`);
await this.refreshAccessToken();
return this.fetch(url, numRetries + 1);
}
throw new Error(
- `Error response from Superset API. Status: ${result.statusCode}, body: ${result.body}`,
+ `Error response from Superset API. Status: ${result.status}, body: ${result.body}`,
);
}
- return result.body as T;
+ try {
+ const json = await result.json();
+ return json as T;
+ } catch (e) {
+ throw new Error(`Invalid response ${e}`);
+ }
}
protected getServerVariable(variableName: string) {
@@ -96,43 +94,31 @@ export class SupersetApi {
};
const url = `${this.baseUrl}/api/v1/security/login`;
- const fetchConfig: any = {
+ const options: RequestInit = {
+ method: 'post',
+ body: JSON.stringify(body),
headers: { 'Content-Type': 'application/json' },
};
- const result = await this.apiRequest('post', url, body, fetchConfig);
+ const result = await this.apiRequest(url, options);
- if (result.statusCode !== 200) {
+ if (result.status !== 200) {
throw new Error(
- `Superset failed to refresh access token. Status: ${result.statusCode}, body: ${result.body}`,
+ `Superset failed to refresh access token. Status: ${result.status}, body: ${result.body}`,
);
}
- const { access_token } = result.body as SecurityLoginResponseBodySchema;
- this.accessToken = access_token;
+ try {
+ const json = await result.json();
+ const { access_token } = json as SecurityLoginResponseBodySchema;
+ this.accessToken = access_token;
+ } catch (e) {
+ throw new Error(`Invalid response ${e}`);
+ }
}
- protected async apiRequest(
- method: NeedleHttpVerbs,
- url: string,
- reqBody: NeedleBodyData = {},
- options: NeedleOptions = {},
- ): Promise {
- // We use the `needle` package instead of the built-in `node-fetch` package
- // because the fetch package does not let us set rejectUnauthorized=false
- // to avoid SSL issues. It only lets us set agent, but we need to be able
- // to set a proxy as the agent instead. So we have to use something that lets
- // us do this, and needle is one such package.
- const opts: any = {
- ...options,
- };
- if (this.insecure) opts.rejectUnauthorized = false;
- if (this.proxyAgent) opts.agent = this.proxyAgent;
- winston.info(`Superset request ${method} ${url}`);
-
- // TODO: opts.rejectUnauthorized not working, bad workaround for now
- process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
- const x = await needle(method, url, reqBody, opts);
- process.env.NODE_TLS_REJECT_UNAUTHORIZED = undefined;
- return x;
+ protected async apiRequest(url: string, options: RequestInit = {}): Promise {
+ if (this.proxyAgent) options.agent = this.proxyAgent;
+ winston.info(`Superset request ${options.method} ${url}`);
+ return fetch(url, options);
}
}
diff --git a/yarn.lock b/yarn.lock
index 0b0575f05f..c7c58128d5 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5627,7 +5627,7 @@ __metadata:
"@tupaia/utils": 1.0.0
"@types/needle": ^2.5.3
https-proxy-agent: ^5.0.1
- needle: ^3.1.0
+ node-fetch: ^1.7.3
winston: ^3.3.3
languageName: unknown
linkType: soft
@@ -17980,7 +17980,7 @@ __metadata:
languageName: node
linkType: hard
-"iconv-lite@npm:^0.6.2, iconv-lite@npm:^0.6.3":
+"iconv-lite@npm:^0.6.2":
version: 0.6.3
resolution: "iconv-lite@npm:0.6.3"
dependencies:
@@ -24413,19 +24413,6 @@ __metadata:
languageName: node
linkType: hard
-"needle@npm:^3.1.0":
- version: 3.1.0
- resolution: "needle@npm:3.1.0"
- dependencies:
- debug: ^3.2.6
- iconv-lite: ^0.6.3
- sax: ^1.2.4
- bin:
- needle: bin/needle
- checksum: 662c8a019d0b2b30137f43e1641aa03d96f9da7ce0d3951af8d6d23c1526c123a992d82fcf9f4e68cba6a52e361a7decfb2c71a56cc0e60230248e5a3520f6ad
- languageName: node
- linkType: hard
-
"negotiator@npm:0.6.2":
version: 0.6.2
resolution: "negotiator@npm:0.6.2"
From b9f8c83dbed00b8592dc8f256caaee6df833336c Mon Sep 17 00:00:00 2001
From: Igor
Date: Tue, 15 Nov 2022 17:36:03 +1100
Subject: [PATCH 5/5] [no-issue]: Fix meditrak app build (#4264)
* Try frozen lockfile
* Hide output spam
* Bump react-native version
https://github.com/facebook/react-native/issues/35210
---
packages/meditrak-app/appcenter-post-clone.sh | 2 +-
packages/meditrak-app/appcenter-pre-build.sh | 3 ++-
packages/meditrak-app/package.json | 2 +-
yarn.lock | 10 +++++-----
4 files changed, 9 insertions(+), 8 deletions(-)
diff --git a/packages/meditrak-app/appcenter-post-clone.sh b/packages/meditrak-app/appcenter-post-clone.sh
index da806169b8..ebf3e3e647 100755
--- a/packages/meditrak-app/appcenter-post-clone.sh
+++ b/packages/meditrak-app/appcenter-post-clone.sh
@@ -21,7 +21,7 @@ nvm use
cd ../..
# install root dependencies
-SKIP_BUILD_INTERNAL_DEPENDENCIES=true yarn install
+SKIP_BUILD_INTERNAL_DEPENDENCIES=true yarn install --frozen-lockfile
# move to meditrak folder
cd packages/meditrak-app
diff --git a/packages/meditrak-app/appcenter-pre-build.sh b/packages/meditrak-app/appcenter-pre-build.sh
index 9913c1f9e0..5040421676 100755
--- a/packages/meditrak-app/appcenter-pre-build.sh
+++ b/packages/meditrak-app/appcenter-pre-build.sh
@@ -14,5 +14,6 @@ else
# install ndk 21.0.6113669 as it's required by realm (https://github.com/realm/realm-js/issues/4740)
SDKMANAGER=$ANDROID_HOME/tools/bin/sdkmanager
- echo y | $SDKMANAGER "ndk;21.0.6113669"
+ # `grep -v = || true` used to hide unnecessary huge output (https://stackoverflow.com/a/52464819)
+ echo y | $SDKMANAGER "ndk;21.0.6113669" | grep -v = || true
fi
diff --git a/packages/meditrak-app/package.json b/packages/meditrak-app/package.json
index e92cc5eda8..287f9420b2 100644
--- a/packages/meditrak-app/package.json
+++ b/packages/meditrak-app/package.json
@@ -49,7 +49,7 @@
"npm-bump": "^0.0.23",
"prop-types": "^15.7.2",
"react": "16.13.1",
- "react-native": "^0.63.4",
+ "react-native": "^0.63.5",
"react-native-database": "^0.1.7",
"react-native-device-info": "^7.2.1",
"react-native-dotenv": "^0.2.0",
diff --git a/yarn.lock b/yarn.lock
index c7c58128d5..937915d2bc 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5462,7 +5462,7 @@ __metadata:
npm-bump: ^0.0.23
prop-types: ^15.7.2
react: 16.13.1
- react-native: ^0.63.4
+ react-native: ^0.63.5
react-native-database: ^0.1.7
react-native-device-info: ^7.2.1
react-native-dotenv: ^0.2.0
@@ -28931,9 +28931,9 @@ __metadata:
languageName: node
linkType: hard
-"react-native@npm:^0.63.4":
- version: 0.63.4
- resolution: "react-native@npm:0.63.4"
+"react-native@npm:^0.63.5":
+ version: 0.63.5
+ resolution: "react-native@npm:0.63.5"
dependencies:
"@babel/runtime": ^7.0.0
"@react-native-community/cli": ^4.10.0
@@ -28966,7 +28966,7 @@ __metadata:
react: 16.13.1
bin:
react-native: ./cli.js
- checksum: 6a603230dda3ce5a3bf093420727148dc157629e05f577f317382fa0384c59dfee796031736a653bc9743b8bb79e014613614ee27f9fa05b3733baffbade3958
+ checksum: 9d59867c9b59b84317d3522d4c947fcb6a452b0e535811874e75d8d32ebcd8655ac15cdeb8e792b132aee84b763b09f80a4be08f1d88a726157b13b7fcb841dd
languageName: node
linkType: hard