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