diff --git a/.circleci/config.yml b/.circleci/config.yml index af1f09b8a3..a57248f6da 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,9 +1,11 @@ version: 2.1 orbs: - aws-cli: circleci/aws-cli@3.1 + aws-cli: circleci/aws-cli@4.1.1 + aws-ecs: circleci/aws-ecs@4.0.0 opsgenie: opsgenie/opsgenie@1.0.8 + jobs: frontend-code-test: resource_class: large @@ -68,7 +70,7 @@ jobs: TM_ORG_CODE: "CICode" TM_ORG_NAME: "CircleCI Test Organisation" - - image: cimg/postgres:14.2-postgis + - image: cimg/postgres:14.9-postgis environment: POSTGRES_USER: taskingmanager POSTGRES_DB: test_tm @@ -79,12 +81,6 @@ jobs: - run: sudo apt-get update - run: sudo apt-get -y install libgeos-dev # Required for shapely - run: sudo apt-get -y install proj-bin libproj-dev - - run: - name: Configure Postgresql Test database - command: | - psql \ - -d $SQLALCHEMY_DATABASE_URI \ - -c "CREATE EXTENSION postgis;" - run: pip install --upgrade pip pdm - run: pdm config --global python.use_venv False - run: pdm export --dev --without-hashes > requirements.txt @@ -113,13 +109,13 @@ jobs: description: "Cloudformation stack name" type: string docker: - - image: cimg/postgres:15.1-postgis + - image: cimg/postgres:15.4-postgis steps: - aws-cli/setup: - role-arn: "arn:aws:iam::$ORG_AWS_ACCOUNT_ID:role/CircleCI-OIDC-Connect" - profile-name: "OIDC-Profile" - role-session-name: "database-snapshot" - session-duration: "2700" + role_arn: "arn:aws:iam::$ORG_AWS_ACCOUNT_ID:role/CircleCI-OIDC-Connect" + profile_name: "OIDC-Profile" + role_session_name: "database-snapshot" + session_duration: "2700" - run: name: Find the instance ID of the database in the stack to backup command: | @@ -193,10 +189,10 @@ jobs: steps: - checkout - aws-cli/setup: - role-arn: "arn:aws:iam::$ORG_AWS_ACCOUNT_ID:role/CircleCI-OIDC-Connect" - profile-name: "OIDC-Profile" - role-session-name: "backend-deploy" - session-duration: "2700" + role_arn: "arn:aws:iam::$ORG_AWS_ACCOUNT_ID:role/CircleCI-OIDC-Connect" + profile_name: "OIDC-Profile" + role_session_name: "backend-deploy" + session_duration: "2700" - run: sudo apt-get update - run: sudo apt-get -y install libgeos-dev jq - run: sudo yarn global add @mapbox/cfn-config @mapbox/cloudfriend @@ -222,6 +218,21 @@ jobs: export JSON_CONFIG="$(< $CIRCLE_WORKING_DIRECTORY/cfn-config-<< parameters.stack_name >>.json)" cfn-config update << parameters.stack_name >> $CIRCLE_WORKING_DIRECTORY/scripts/aws/cloudformation/tasking-manager.template.js -f -c hot-cfn-config -t hot-cfn-config -r $AWS_REGION -p "$JSON_CONFIG" + backend_deploy_containers: + working_directory: /home/circleci/tasking-manager + docker: + - image: cimg/python:3.10.7 + steps: + - checkout + - aws-cli/setup: + role_arn: "arn:aws:iam::$ORG_AWS_ACCOUNT_ID:role/CircleCI-OIDC-Connect" + profile_name: "OIDC-Profile" + role_session_name: "backend-deploy-containers" + session_duration: "2700" + - run: sudo apt-get update + - run: sudo apt-get -y install curl + - run: echo "Run AWS Fargate" + frontend_deploy: working_directory: /home/circleci/tasking-manager resource_class: large @@ -234,10 +245,10 @@ jobs: steps: - checkout - aws-cli/setup: - role-arn: "arn:aws:iam::$ORG_AWS_ACCOUNT_ID:role/CircleCI-OIDC-Connect" - profile-name: "OIDC-Profile" - role-session-name: "frontend-deploy" - session-duration: "1800" + role_arn: "arn:aws:iam::$ORG_AWS_ACCOUNT_ID:role/CircleCI-OIDC-Connect" + profile_name: "OIDC-Profile" + role_session_name: "frontend-deploy" + session_duration: "1800" - run: name: Deploy Frontend to S3 command: | diff --git a/.github/labeler.yml b/.github/labeler.yml index 731d49ea7b..9a90dea7b1 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -1,12 +1,12 @@ -"Component: Frontend": +"codebase: frontend": - frontend/**/* -"Component: Backend": +"codebase: backend": - backend/**/* - tests/**/* - migrations/**/* - ./manage.py - ./pyproject.toml -"Component: Infrastructure": +"infrastructure": - .circleci/* - .github/**/* - scripts/aws/**/* @@ -20,5 +20,5 @@ - frontend/yarn.lock "python": - ./pyproject.toml -"Type: Translations": +"type: translations": - frontend/src/locales/* diff --git a/README.md b/README.md index f5317df6ad..90750c3c2d 100644 --- a/README.md +++ b/README.md @@ -18,9 +18,7 @@ This is Free and Open Source Software. You are welcome to use the code and set u * Read the monthly update blogs on [OSM Discourse](https://community.openstreetmap.org/c/general/38/all). ## Product Roadmap -We have included below a high level roadmap/plan [subject to change] that can be used as an overview. -![image](https://user-images.githubusercontent.com/98902727/218763601-f08e3879-51f3-40a7-ae6e-bdf96f8a5979.png) - +We have included below a [high level roadmap/plan](https://github.com/orgs/hotosm/projects/28/) [subject to change] that can be used as an overview. ## Developers diff --git a/backend/api/teams/resources.py b/backend/api/teams/resources.py index 3f5c13b848..06df030d27 100644 --- a/backend/api/teams/resources.py +++ b/backend/api/teams/resources.py @@ -181,8 +181,7 @@ def delete(self, team_id): "SubCode": "UserNotTeamManager", }, 401 - TeamService.delete_team(team_id) - return {"Success": "Team deleted"}, 200 + return TeamService.delete_team(team_id) class TeamsAllAPI(Resource): diff --git a/backend/config.py b/backend/config.py index a76256e6d4..555205014f 100644 --- a/backend/config.py +++ b/backend/config.py @@ -205,7 +205,7 @@ class EnvironmentConfig: """ import json - _params = json.loads(os.getenv("OAUTH2_APP_CREDENTIALS"), None) + _params = json.loads(os.getenv("OAUTH2_APP_CREDENTIALS", None)) OAUTH_CLIENT_ID = _params.get("CLIENT_ID", None) OAUTH_CLIENT_SECRET = _params.get("CLIENT_SECRET", None) OAUTH_REDIRECT_URI = _params.get("REDIRECT_URI", None) diff --git a/backend/services/team_service.py b/backend/services/team_service.py index 292360e3b8..6eeaf7b5b3 100644 --- a/backend/services/team_service.py +++ b/backend/services/team_service.py @@ -566,8 +566,12 @@ def delete_team(team_id: int): if team.can_be_deleted(): team.delete() + return {"Success": "Team deleted"}, 200 else: - raise TeamServiceError("Team has projects, cannot be deleted") + return { + "Error": "Team has projects, cannot be deleted", + "SubCode": "This team has projects associated. Before deleting team, unlink any associated projects.", + }, 400 @staticmethod def check_team_membership(project_id: int, allowed_roles: list, user_id: int): diff --git a/example.env b/example.env index b02511f8f1..51c2e37818 100644 --- a/example.env +++ b/example.env @@ -84,10 +84,10 @@ OSM_REGISTER_URL=https://www.openstreetmap.org/user/new # It's not required to set this tag. Case it isn't set, an image will be used as background. # TM_HOMEPAGE_VIDEO_URL= -# Endpoint for the missing maps stats +# API base URL and token(used to retrieve user stats only) for ohsomeNow Stats # -TM_USER_STATS_API_URL=https://osm-stats-production-api.azurewebsites.net/users/ -TM_HOMEPAGE_STATS_API_URL=https://osmstats-api.hotosm.org/wildcard?key=hotosm-project-* +OHSOME_STATS_BASE_URL=https://stats.now.ohsome.org/api +OHSOME_STATS_TOKEN=testSuperSecretTestToken # Secret (required) # diff --git a/frontend/.env.expand b/frontend/.env.expand index df7cb9d0ba..29585a9c42 100644 --- a/frontend/.env.expand +++ b/frontend/.env.expand @@ -29,8 +29,8 @@ REACT_APP_MAPBOX_TOKEN=$TM_MAPBOX_TOKEN REACT_APP_ENABLE_SERVICEWORKER=$TM_ENABLE_SERVICEWORKER REACT_APP_MAX_FILESIZE=$TM_IMPORT_MAX_FILESIZE REACT_APP_MAX_AOI_AREA=$TM_MAX_AOI_AREA -REACT_APP_USER_STATS_API_URL=$TM_USER_STATS_API_URL -REACT_APP_HOMEPAGE_STATS_API_URL=$TM_HOMEPAGE_STATS_API_URL +REACT_APP_OHSOME_STATS_BASE_URL=$OHSOME_STATS_BASE_URL +REACT_APP_OHSOME_STATS_TOKEN=$OHSOME_STATS_TOKEN REACT_APP_OSM_CLIENT_ID=$TM_CLIENT_ID REACT_APP_OSM_CLIENT_SECRET=$TM_CLIENT_SECRET REACT_APP_OSM_REDIRECT_URI=$TM_REDIRECT_URI diff --git a/frontend/src/api/projects.js b/frontend/src/api/projects.js index 66163644b8..f9d46564d3 100644 --- a/frontend/src/api/projects.js +++ b/frontend/src/api/projects.js @@ -37,8 +37,10 @@ export const useProjectsQuery = (fullProjectsQuery, action) => { }; export const useProjectQuery = (projectId) => { + const token = useSelector((state) => state.auth.token); + const locale = useSelector((state) => state.preferences['locale']); const fetchProject = ({ signal }) => { - return api().get(`projects/${projectId}/`, { + return api(token, locale).get(`projects/${projectId}/`, { signal, }); }; diff --git a/frontend/src/api/stats.js b/frontend/src/api/stats.js index c54d0b7332..5bacdd28dc 100644 --- a/frontend/src/api/stats.js +++ b/frontend/src/api/stats.js @@ -1,7 +1,7 @@ import { useQuery } from '@tanstack/react-query'; import api from './apiClient'; -import { HOMEPAGE_STATS_API_URL } from '../config'; +import { OHSOME_STATS_BASE_URL } from '../config'; export const useSystemStatisticsQuery = () => { const fetchSystemStats = ({ signal }) => { @@ -33,7 +33,7 @@ export const useProjectStatisticsQuery = (projectId) => { export const useOsmStatsQuery = () => { const fetchOsmStats = ({ signal }) => { - return api().get(HOMEPAGE_STATS_API_URL, { + return api().get(`${OHSOME_STATS_BASE_URL}/stats/hotosm-project-%2A`, { signal, }); }; @@ -42,20 +42,15 @@ export const useOsmStatsQuery = () => { queryKey: ['osm-stats'], queryFn: fetchOsmStats, useErrorBoundary: true, + select: (data) => data.data.result }); }; export const useOsmHashtagStatsQuery = (defaultComment) => { const fetchOsmStats = ({ signal }) => { - return api().get( - `https://osm-stats-production-api.azurewebsites.net/stats/${defaultComment[0].replace( - '#', - '', - )}`, - { - signal, - }, - ); + return api().get(`${OHSOME_STATS_BASE_URL}/stats/${defaultComment[0].replace('#', '')}`, { + signal, + }); }; return useQuery({ @@ -63,6 +58,6 @@ export const useOsmHashtagStatsQuery = (defaultComment) => { queryFn: fetchOsmStats, useErrorBoundary: true, enabled: Boolean(defaultComment?.[0]), - select: (data) => data.data, + select: (data) => data.data.result, }); }; diff --git a/frontend/src/assets/styles/_extra.scss b/frontend/src/assets/styles/_extra.scss index 49359a7bc4..b5887bdfb5 100644 --- a/frontend/src/assets/styles/_extra.scss +++ b/frontend/src/assets/styles/_extra.scss @@ -616,3 +616,8 @@ a[href="https://www.mapbox.com/map-feedback/"] .link:focus { outline: revert; } + +// Override tachyons font-family for code tag +.code { + font-family: inherit; +} diff --git a/frontend/src/components/homepage/stats.js b/frontend/src/components/homepage/stats.js index da36a3e1aa..1901fa35f4 100644 --- a/frontend/src/components/homepage/stats.js +++ b/frontend/src/components/homepage/stats.js @@ -42,15 +42,15 @@ export const StatsSection = () => {
user.username)} + contributors={ + Array.isArray(contributors) ? contributors.map((user) => user.username) : undefined + } />
diff --git a/frontend/src/components/projectStats/edits.js b/frontend/src/components/projectStats/edits.js index 04a8a96164..7f76e9733f 100644 --- a/frontend/src/components/projectStats/edits.js +++ b/frontend/src/components/projectStats/edits.js @@ -1,14 +1,13 @@ import React from 'react'; -import ReactTooltip from 'react-tooltip'; -import { FormattedMessage, useIntl } from 'react-intl'; +import { FormattedMessage } from 'react-intl'; import projectMessages from './messages'; import userDetailMessages from '../userDetail/messages'; -import { MappingIcon, HomeIcon, RoadIcon, EditIcon, InfoIcon } from '../svgIcons'; +import { MappingIcon, HomeIcon, RoadIcon, EditIcon } from '../svgIcons'; import { StatsCard } from '../statsCard'; +import StatsTimestamp from '../statsTimestamp'; export const EditsStats = ({ data }) => { - const intl = useIntl(); const { changesets, buildings, roads, edits } = data; const iconClass = 'h-50 w-50'; @@ -16,14 +15,12 @@ export const EditsStats = ({ data }) => { return (
-

- - -

- +
+

+ +

+ +
{ hasPrev: false, page: 1, pages: 3, + total: 10, }} />, ); @@ -32,6 +33,7 @@ describe('ProjectCardPaginator Component', () => { hasPrev: false, page: 1, pages: 3, + total: 10, }} />, ); diff --git a/frontend/src/components/rapidEditor.js b/frontend/src/components/rapidEditor.js index fc5f833c81..05fbc219f9 100644 --- a/frontend/src/components/rapidEditor.js +++ b/frontend/src/components/rapidEditor.js @@ -1,6 +1,8 @@ import { useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; +import PropTypes from 'prop-types'; + import { OSM_CLIENT_ID, OSM_CLIENT_SECRET, OSM_REDIRECT_URI, OSM_SERVER_URL } from '../config'; import { types } from '../store/actions/editor'; @@ -278,5 +280,15 @@ function RapidEditor({ return
; } +RapidEditor.propTypes = { + setDisable: PropTypes.func, + comment: PropTypes.string, + presets: PropTypes.array, + imagery: PropTypes.string, + gpxUrl: PropTypes.string.isRequired, + powerUser: PropTypes.bool.isRequired, + showSidebar: PropTypes.bool.isRequired, +}; + export { RapidEditor, generateStartingHash, equalsUrlParameters, updateUrl }; export default RapidEditor; diff --git a/frontend/src/components/statsTimestamp/index.js b/frontend/src/components/statsTimestamp/index.js new file mode 100644 index 0000000000..447ac28351 --- /dev/null +++ b/frontend/src/components/statsTimestamp/index.js @@ -0,0 +1,45 @@ +import { useState, useEffect } from 'react'; +import { useIntl } from 'react-intl'; +import ReactTooltip from 'react-tooltip'; + +import { fetchExternalJSONAPI } from '../../network/genericJSONRequest'; +import { OHSOME_STATS_BASE_URL } from '../../config'; +import { InfoIcon } from '../svgIcons'; +import messages from './messages'; + +function StatsTimestamp({ messageType }) { + const intl = useIntl(); + const [lastUpdated, setLastUpdated] = useState(null); + + useEffect(() => { + fetchExternalJSONAPI(`${OHSOME_STATS_BASE_URL}/metadata`) + .then((res) => { + setLastUpdated(res.result.max_timestamp); + }) + .catch((error) => console.error(error)); + }, []); + + const dateOptions = { + year: 'numeric', + month: 'short', + day: '2-digit', + hour: 'numeric', + minute: 'numeric', + }; + + return ( +
+ + +
+ ); +} + +export default StatsTimestamp; diff --git a/frontend/src/components/statsTimestamp/messages.js b/frontend/src/components/statsTimestamp/messages.js new file mode 100644 index 0000000000..5151378ecb --- /dev/null +++ b/frontend/src/components/statsTimestamp/messages.js @@ -0,0 +1,14 @@ +import { defineMessages } from 'react-intl'; + +export default defineMessages({ + generic: { + id: 'stats.ohsome.timestamp.generic', + defaultMessage: + 'These statistics come from ohsomeNow Stats and were last updated at {formattedDate} ({timeZone}). Missing fields will be made available soon!', + }, + project: { + id: 'stats.ohsome.timestamp.project', + defaultMessage: + 'These stats were retrieved using the default changeset comment of the project and were last updated at {formattedDate} ({timeZone}).', + }, +}); diff --git a/frontend/src/components/taskSelection/actionSidebars.js b/frontend/src/components/taskSelection/actionSidebars.js index cdfd3e2b86..4dcceaed3f 100644 --- a/frontend/src/components/taskSelection/actionSidebars.js +++ b/frontend/src/components/taskSelection/actionSidebars.js @@ -185,9 +185,10 @@ export function CompletionTabForMapping({ )} {showReadCommentsAlert && (
historyTabSwitch()} + onKeyDown={() => {}} > @@ -586,19 +587,12 @@ const TaskValidationSelector = ({ // the contributors is filled only on the case of single task validation, // so we need to fetch the task history in the case of multiple task validation useEffect(() => { - if (showCommentInput && isValidatingMultipleTasks && !contributors.length) { + if (showCommentInput && isValidatingMultipleTasks) { fetchLocalJSONAPI(`projects/${projectId}/tasks/${id}/`).then((response) => setContributorsList(getTaskContributors(response.taskHistory, userDetails.username)), ); } - }, [ - isValidatingMultipleTasks, - showCommentInput, - contributors, - id, - projectId, - userDetails.username, - ]); + }, [isValidatingMultipleTasks, showCommentInput, id, projectId, userDetails.username]); return (
@@ -650,7 +644,7 @@ const TaskValidationSelector = ({ setVisibility(false)} + onKeyDown={() => {}} > @@ -834,6 +829,7 @@ function TaskSpecificInstructions({ instructions, open = true }: Object) { className="ttu blue-grey mt1 mb0 pointer" role="button" onClick={() => setIsOpen(!isOpen)} + onKeyDown={() => {}} > {isOpen ? ( diff --git a/frontend/src/components/taskSelection/tests/actionSidebars.test.js b/frontend/src/components/taskSelection/tests/actionSidebars.test.js index 413881ec70..bfd2770f9b 100644 --- a/frontend/src/components/taskSelection/tests/actionSidebars.test.js +++ b/frontend/src/components/taskSelection/tests/actionSidebars.test.js @@ -19,12 +19,16 @@ import { import { setupFaultyHandlers } from '../../../network/tests/server'; import messages from '../messages'; import { store } from '../../../store'; +import { TaskMapAction } from '../action'; +import { getProjectSummary } from '../../../network/tests/mockData/projects'; +import tasksGeojson from '../../../utils/tests/snippets/tasksGeometry'; +import { userMultipleLockedTasksDetails } from '../../../network/tests/mockData/userStats'; jest.mock('react-hot-toast', () => ({ error: jest.fn(), })); -describe('Appeareance of unsaved map changes to be dealt with while mapping', () => { +describe('Appearance of unsaved map changes to be dealt with while mapping', () => { test('when splitting a task', async () => { const { user } = renderWithRouter( @@ -183,7 +187,7 @@ describe('Miscellaneous modals and prompts', () => { }); }); -describe('Appeareance of unsaved map changes to be dealt with while validating', () => { +describe('Appearance of unsaved map changes to be dealt with while validating', () => { test('when stopping validation session', async () => { const { user } = renderWithRouter( @@ -340,31 +344,34 @@ describe('Toggling display of the sidebar', () => { }); it('should call the sidebar toggle function for RAPID editor', async () => { - const restartMock = jest.fn(); + // Testing the resize call cannot be done currently, due to the following reasons: + // 1. Jest cannot mock/spy on the function call + // 2. The test environment doesn't have width/height information + // 3. The resize call in Rapid cannot be mocked since it is difficult to (a) get the context and (b) mock the call prior to full initialization. + // const resizeMock = jest.fn(); + // expect(resizeMock).toHaveBeenCalledTimes(1); // This should be at the end of the test const user = userEvent.setup(); - const context = { - ui: jest.fn().mockReturnValue({ - restart: restartMock, - }), - }; - act(() => { - store.dispatch({ type: 'SET_RAPIDEDITOR', context: context }); - }); - const setShowSidebarMock = jest.fn(); - render( + const { getByRole, queryByRole } = renderWithRouter( - + , ); await user.click( - screen.getByRole('button', { - name: /hide sidebar/i, + getByRole('button', { + name: 'Hide sidebar', }), ); - expect(setShowSidebarMock).toHaveBeenCalledTimes(1); - expect(restartMock).toHaveBeenCalledTimes(1); + expect(getByRole('generic', { name: 'Show sidebar' })).toBeVisible(); + expect(queryByRole('button', { name: 'Hide sidebar' })).toBeNull(); }); }); diff --git a/frontend/src/components/teamsAndOrgs/featureStats.js b/frontend/src/components/teamsAndOrgs/featureStats.js index 6b6e8f9968..ffeeb5bd31 100644 --- a/frontend/src/components/teamsAndOrgs/featureStats.js +++ b/frontend/src/components/teamsAndOrgs/featureStats.js @@ -2,20 +2,25 @@ import React, { useEffect, useState } from 'react'; import axios from 'axios'; import { FormattedMessage } from 'react-intl'; +import messages from './messages'; import userDetailMessages from '../userDetail/messages'; -import { HOMEPAGE_STATS_API_URL } from '../../config'; +import { OHSOME_STATS_BASE_URL } from '../../config'; import { RoadIcon, HomeIcon, WavesIcon, MarkerIcon } from '../svgIcons'; import { StatsCard } from '../statsCard'; +import StatsTimestamp from '../statsTimestamp'; export const FeatureStats = () => { const [stats, setStats] = useState({ edits: 0, buildings: 0, roads: 0, pois: 0, waterways: 0 }); const getStats = async () => { try { - const response = await axios.get(HOMEPAGE_STATS_API_URL); + const response = await axios.get( + `${OHSOME_STATS_BASE_URL}/stats/hotosm-project-%2A`, + ); + const { edits, buildings, roads } = response.data.result; setStats({ - edits: response.data.edits, - buildings: response.data.building_count_add, - roads: response.data.road_km_add, + edits, + buildings, + roads, pois: response.data.poi_count_add, waterways: response.data.waterway_km_add, }); @@ -32,31 +37,39 @@ export const FeatureStats = () => { const iconStyle = { height: '45px' }; return ( -
- } - description={} - value={stats.buildings || 0} - className={'w-25-l w-50-m w-100 mv1'} - /> - } - description={} - value={stats.roads || 0} - className={'w-25-l w-50-m w-100 mv1'} - /> - } - description={} - value={stats.pois || 0} - className={'w-25-l w-50-m w-100 mv1'} - /> - } - description={} - value={stats.waterways || 0} - className={'w-25-l w-50-m w-100 mv1'} - /> -
+ <> +
+

+ +

+ +
+
+ } + description={} + value={stats.buildings || 0} + className={'w-25-l w-50-m w-100 mv1'} + /> + } + description={} + value={stats.roads || 0} + className={'w-25-l w-50-m w-100 mv1'} + /> + } + description={} + value={stats.pois || 0} + className={'w-25-l w-50-m w-100 mv1'} + /> + } + description={} + value={stats.waterways || 0} + className={'w-25-l w-50-m w-100 mv1'} + /> +
+ ); }; diff --git a/frontend/src/components/teamsAndOrgs/messages.js b/frontend/src/components/teamsAndOrgs/messages.js index 74e77d5834..ace6caf287 100644 --- a/frontend/src/components/teamsAndOrgs/messages.js +++ b/frontend/src/components/teamsAndOrgs/messages.js @@ -553,4 +553,8 @@ export default defineMessages({ id: 'management.stats.overview', defaultMessage: 'Overview', }, + totalFeatures: { + id: 'management.stats.features', + defaultMessage: 'Total features', + }, }); diff --git a/frontend/src/components/teamsAndOrgs/tests/featureStats.test.js b/frontend/src/components/teamsAndOrgs/tests/featureStats.test.js index 0562cf4cdf..1dae46c965 100644 --- a/frontend/src/components/teamsAndOrgs/tests/featureStats.test.js +++ b/frontend/src/components/teamsAndOrgs/tests/featureStats.test.js @@ -23,6 +23,7 @@ test('FeatureStats renders the correct values and labels', async () => { expect(screen.getByText('Km waterways mapped')).toBeInTheDocument(); await waitFor(() => expect(screen.getByText('2,380,562')).toBeInTheDocument()); expect(screen.getByText('101,367,027')).toBeInTheDocument(); - expect(screen.getByText('183,011')).toBeInTheDocument(); - expect(screen.getByText('350,906')).toBeInTheDocument(); + // Uncomment the following when POIs and waterways become available + // expect(screen.getByText('183,011')).toBeInTheDocument(); + // expect(screen.getByText('350,906')).toBeInTheDocument(); }); diff --git a/frontend/src/components/userDetail/editsByNumbers.js b/frontend/src/components/userDetail/editsByNumbers.js index 67720a8583..45546d9ec5 100644 --- a/frontend/src/components/userDetail/editsByNumbers.js +++ b/frontend/src/components/userDetail/editsByNumbers.js @@ -15,13 +15,13 @@ const EditsByNumbers = ({ osmStats }) => { let reference = [ { label: intl.formatMessage(typesMessages.buildings), - field: 'total_building_count_add', + field: 'buildings', backgroundColor: CHART_COLOURS.red, borderColor: CHART_COLOURS.white, }, { label: intl.formatMessage(typesMessages.roads), - field: 'total_road_km_add', + field: 'roads', backgroundColor: CHART_COLOURS.green, borderColor: CHART_COLOURS.white, }, diff --git a/frontend/src/components/userDetail/elementsMapped.js b/frontend/src/components/userDetail/elementsMapped.js index f8cebf460f..72c9a58126 100644 --- a/frontend/src/components/userDetail/elementsMapped.js +++ b/frontend/src/components/userDetail/elementsMapped.js @@ -1,6 +1,5 @@ import React from 'react'; import humanizeDuration from 'humanize-duration'; -import ReactTooltip from 'react-tooltip'; import { FormattedMessage } from 'react-intl'; import messages from './messages'; @@ -10,11 +9,11 @@ import { HomeIcon, WavesIcon, MarkerIcon, - QuestionCircleIcon, MappedIcon, ValidatedIcon, } from '../svgIcons'; import { StatsCard } from '../statsCard'; +import StatsTimestamp from '../statsTimestamp'; export const TaskStats = ({ userStats, username }) => { const { @@ -137,35 +136,26 @@ export const ElementsMapped = ({ userStats, osmStats }) => { } description={} - value={osmStats.total_building_count_add || 0} + value={osmStats.buildings || 0} /> } description={} - value={osmStats.total_road_km_add || 0} + value={osmStats.roads || 0} /> } description={} - value={osmStats.total_poi_count_add || 0} + value={osmStats.total_poi_count_add || '-'} /> } description={} - value={osmStats.total_waterway_km_add || 0} + value={osmStats.total_waterway_km_add || '-'} />
- - {(msg) => ( - - )} - - +
); diff --git a/frontend/src/components/userDetail/messages.js b/frontend/src/components/userDetail/messages.js index 209382b094..fa4035a956 100644 --- a/frontend/src/components/userDetail/messages.js +++ b/frontend/src/components/userDetail/messages.js @@ -121,11 +121,6 @@ export default defineMessages({ id: 'users.detail.heatmapLegendLess', defaultMessage: 'less', }, - delayPopup: { - id: 'users.detail.delay_popup', - defaultMessage: - 'These statistics need heavy calculations and changes are showing up with a delay of around one hour.', - }, teams: { id: 'users.detail.teams', defaultMessage: 'Teams', diff --git a/frontend/src/components/userDetail/tests/editByNumbers.test.js b/frontend/src/components/userDetail/tests/editByNumbers.test.js index 4b4f653ae6..e8d370e897 100644 --- a/frontend/src/components/userDetail/tests/editByNumbers.test.js +++ b/frontend/src/components/userDetail/tests/editByNumbers.test.js @@ -27,12 +27,10 @@ describe('EditsByNumbers card', () => { it('renders the chart if osmStats data is present', () => { const stats = { - total_building_count_add: 3282, - total_building_count_mod: 7959, + buildings: 3282, total_waterway_count_add: 11493, total_poi_count_add: 10805, - total_road_km_add: 5571.84370201545, - total_road_km_mod: 4203.47860727417, + roads: 5571.84370201545, total_waterway_km_add: 512.706405358494, total_road_count_add: 13345, total_road_count_mod: 51730, diff --git a/frontend/src/components/userDetail/tests/elementsMapped.test.js b/frontend/src/components/userDetail/tests/elementsMapped.test.js index 0350156d19..592e1e6a68 100644 --- a/frontend/src/components/userDetail/tests/elementsMapped.test.js +++ b/frontend/src/components/userDetail/tests/elementsMapped.test.js @@ -12,7 +12,7 @@ describe('ElementsMapped & TaskStats components', () => { }; const osmStats = { total_building_count_add: 10, - total_road_km_add: 229.113, + roads: 229.113, total_poi_count_add: 15, total_waterway_count_add: 20, }; diff --git a/frontend/src/config/index.js b/frontend/src/config/index.js index 1b0845da0a..41074cd781 100644 --- a/frontend/src/config/index.js +++ b/frontend/src/config/index.js @@ -3,12 +3,9 @@ export const API_VERSION = process.env.REACT_APP_API_VERSION || 'v2'; export const API_URL = process.env.REACT_APP_API_URL ? new URL('/api/' + API_VERSION + '/', process.env.REACT_APP_API_URL) : 'http://127.0.0.1:5000/api/' + API_VERSION + '/'; -export const HOMEPAGE_STATS_API_URL = - process.env.REACT_APP_HOMEPAGE_STATS_API_URL || - 'https://osmstats-api.hotosm.org/wildcard/?key=hotosm-project-*'; -export const USER_STATS_API_URL = - process.env.REACT_APP_USER_STATS_API_URL || - 'https://osm-stats-production-api.azurewebsites.net/users/'; +export const OHSOME_STATS_BASE_URL = + process.env.REACT_APP_OHSOME_STATS_BASE_URL || 'https://stats.now.ohsome.org/api'; +export const OHSOME_STATS_TOKEN = process.env.REACT_APP_OHSOME_STATS_TOKEN || ''; // APPLICATION SETTINGS export const DEFAULT_LOCALE = process.env.REACT_APP_DEFAULT_LOCALE || 'en'; diff --git a/frontend/src/config/tests/config.test.js b/frontend/src/config/tests/config.test.js index 795c2427ca..67e100878f 100644 --- a/frontend/src/config/tests/config.test.js +++ b/frontend/src/config/tests/config.test.js @@ -7,11 +7,8 @@ it('exports API_URL', () => { it('exports API_VERSION', () => { expect(['object', 'string']).toContain(typeof config.API_VERSION); }); -it('exports HOMEPAGE_STATS_API_URL', () => { - expect(typeof config.HOMEPAGE_STATS_API_URL).toBe('string'); -}); -it('exports USER_STATS_API_URL', () => { - expect(typeof config.USER_STATS_API_URL).toBe('string'); +it('exports OHSOME_STATS_BASE_URL', () => { + expect(typeof config.OHSOME_STATS_BASE_URL).toBe('string'); }); it('exports ORG_URL', () => { expect(typeof config.ORG_URL).toBe('string'); diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index e74401293b..38d961f9fa 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -616,7 +616,8 @@ "project.stats.totalEdits": "Total map edits", "project.stats.changesets": "Changesets", "project.stats.edits": "Edits", - "project.stats.edits.info": "These stats are retrieved using the default changeset comment of the project", + "stats.ohsome.timestamp.generic": "These statistics come from ohsomeNow Stats and were last updated at {formattedDate} ({timeZone}). Missing fields will be made available soon!", + "stats.ohsome.timestamp.project": "These stats were retrieved using the default changeset comment of the project and were last updated at {formattedDate} ({timeZone}).", "project.tasks.unsaved_map_changes.title": "You have some unsaved map changes", "project.tasks.unsaved_map_changes.split": "Save or undo it to be able to split the task", "project.tasks.unsaved_map_changes.unlock": "Save or undo it to be able to select another task", @@ -945,6 +946,7 @@ "management.stats.new_users.email_verified": "Confirmed email address", "management.stats.title": "Statistics", "management.stats.overview": "Overview", + "management.stats.features": "Total features", "user.nextLevel": "{changesets} / {nextLevelThreshold} changesets to {level}", "user.personalInfo": "Personal information", "user.name": "Name", @@ -1065,7 +1067,6 @@ "users.detail.heatmapContributions": "contributions", "users.detail.heatmapLegendMore": "more", "users.detail.heatmapLegendLess": "less", - "users.detail.delay_popup": "These statistics need heavy calculations and changes are showing up with a delay of around one hour.", "users.detail.teams": "Teams", "error.page.title": "An error occurred", "error.page.description": "Something did not work well...", @@ -1083,7 +1084,6 @@ "management.managers": "Managers", "management.users.title": "Manage users", "management.stats.users.title": "New users", - "management.stats.features": "Total features", "teamsAndOrgs.management.organisation.creation": "Create new organization", "teamsAndOrgs.management.organisation.orgCreationNameExistsError": "Organisation name already exists", "teamsAndOrgs.management.organisation.edit": "Edit organization", diff --git a/frontend/src/network/genericJSONRequest.js b/frontend/src/network/genericJSONRequest.js index 9af29cbfe2..f877c9813a 100644 --- a/frontend/src/network/genericJSONRequest.js +++ b/frontend/src/network/genericJSONRequest.js @@ -1,12 +1,19 @@ import { handleErrors } from '../utils/promise'; -import { API_URL } from '../config'; +import { API_URL, OHSOME_STATS_TOKEN } from '../config'; + +export function fetchExternalJSONAPI(url, isSetToken = false): Promise<*> { + const headers = { + 'Content-Type': 'application/json', + }; + + // Passing token only for ohsomeNow stats + if (isSetToken) { + headers['Authorization'] = `Basic ${OHSOME_STATS_TOKEN}`; + } -export function fetchExternalJSONAPI(url): Promise<*> { return fetch(url, { method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, + headers: headers, }) .then(handleErrors) .then((res) => { diff --git a/frontend/src/network/tests/mockData/homepageStats.js b/frontend/src/network/tests/mockData/homepageStats.js index 34422d4b4a..5d5f63300f 100644 --- a/frontend/src/network/tests/mockData/homepageStats.js +++ b/frontend/src/network/tests/mockData/homepageStats.js @@ -1,15 +1,10 @@ export const homepageStats = { - road_count_add: 5850597, - road_count_mod: 6688860, - building_count_add: 101367027, - building_count_mod: 12680746, - waterway_count_add: 3267982, - poi_count_add: 183011, - poi_count_mod: 251157, - road_km_add: 2380562.9900000077, - road_km_mod: 1499361.339999999, - waterway_km_add: 350906.79999999935, - waterway_km_mod: 199147.73999999985, - edits: 134184186, - users: 493238, + result: { + changesets: 10251848, + users: 493238, + roads: 2380562.9900000077, + buildings: 101367027, + edits: 134184186, + latest: '2023-10-03T15:28:07Z', + }, }; diff --git a/frontend/src/network/tests/mockData/miscellaneous.js b/frontend/src/network/tests/mockData/miscellaneous.js index 5ef4c468e4..f28b8cf330 100644 --- a/frontend/src/network/tests/mockData/miscellaneous.js +++ b/frontend/src/network/tests/mockData/miscellaneous.js @@ -34,3 +34,10 @@ export const systemStats = { totalMappers: 3, totalProjects: 10, }; + +export const ohsomeNowMetadata = { + result: { + max_timestamp: '2023-10-03T15:47:49Z', + min_timestamp: '2009-04-21T20:02:04Z', + }, +}; diff --git a/frontend/src/network/tests/mockData/userStats.js b/frontend/src/network/tests/mockData/userStats.js index b151d51324..0e2b1efd38 100644 --- a/frontend/src/network/tests/mockData/userStats.js +++ b/frontend/src/network/tests/mockData/userStats.js @@ -69,126 +69,25 @@ export const userStats = { ], }; -export const osmStatsProd = { - id: 10291369, - name: 'helnershingthapa', - changesets: '361', - geo_extent: null, - total_building_count_add: 6771, - total_building_count_mod: 786, - total_waterway_count_add: 2, - total_poi_count_add: 81, - total_road_km_add: 29.0827659890004, - total_road_km_mod: 34.4722514676097, - total_waterway_km_add: 0.497270130447684, - total_josm_edit_count: 298, - total_gps_trace_count_add: 0, - total_gps_trace_updated_from_osm: 0, - total_road_count_add: 59, - total_road_count_mod: 250, - total_tm_done_count: 0, - total_tm_val_count: 0, - total_tm_inval_count: 0, - badges: [ - { - updated_at: '2020-11-20T12:29:36.265Z', - id: 11, - category: 4, - level: 2, - name: 'The Wright Stuff', - }, - { - updated_at: '2021-08-11T13:05:02.367Z', - id: 12, - category: 4, - level: 3, - name: 'The Wright Stuff', - }, - { - updated_at: '2022-12-28T08:23:30.755Z', - id: 26, - category: 9, - level: 2, - name: 'World Renown', - }, - ], - changeset_count: 361, - latest: { - id: '130862065', - road_count_add: 0, - road_count_mod: 1, - building_count_add: 7, - building_count_mod: 2, - waterway_count_add: 0, - poi_count_add: 0, - gpstrace_count_add: 0, - road_km_add: 0, - road_km_mod: 0.00768819386078029, - waterway_km_add: 0, - gpstrace_km_add: 0, - editor: 'JOSM/1.5 (18583 en)', +export const ohsomeNowUserStats = { + result: { + building_count: 2, + road_length: 572.750505196795, + edits: 12618, user_id: 10291369, - created_at: '2023-01-04T11:15:22.000Z', - countries: [ - { - id: 175, - name: 'Nepal', - code: 'NPL', - }, - ], - hashtags: [ - { - id: 4, - hashtag: 'awesome', - }, - { - id: 2101, - hashtag: 'hot', - }, - { - id: 14913965, - hashtag: 'ootd', - }, - { - id: 17849271, - hashtag: 'art', - }, - ], - }, - edit_times: [ - '2019-09-11T00:00:00.000Z', - '2019-09-12T00:00:00.000Z', - '2019-11-15T00:00:00.000Z', - '2020-01-27T00:00:00.000Z', - '2020-02-28T00:00:00.000Z', - '2020-11-20T00:00:00.000Z', - '2021-02-01T00:00:00.000Z', - '2021-02-04T00:00:00.000Z', - '2021-02-12T00:00:00.000Z', - '2021-02-14T00:00:00.000Z', - ], - country_count: 3, - country_list: { - NPL: 248, - MNG: 28, - IND: 11, - }, - hashtags: { - awesome: 139, - hot: 126, - ootd: 97, - art: 78, + object_edits: 291, }, }; export const osmStatsProject = { - changesets: 987654321, - users: 112, - roads: 5658.62006919192, - buildings: 12923, - edits: 123456789, - latest: '2020-10-05T23:21:22.000Z', - hashtag: `hotosm-project-1`, + result: { + changesets: 987654321, + users: 112, + roads: 5658.62006919192, + buildings: 12923, + edits: 123456789, + latest: '2020-10-05T23:21:22.000Z', + }, }; export const userLockedTasksDetails = { diff --git a/frontend/src/network/tests/server-handlers.js b/frontend/src/network/tests/server-handlers.js index e55b43ae30..66851658df 100644 --- a/frontend/src/network/tests/server-handlers.js +++ b/frontend/src/network/tests/server-handlers.js @@ -18,10 +18,10 @@ import { import { featuredProjects } from './mockData/featuredProjects'; import { newUsersStats, - osmStatsProd, + userStats, osmStatsProject, userLockedTasksDetails, - userStats, + ohsomeNowUserStats, } from './mockData/userStats'; import { projectContributions, projectContributionsByDay } from './mockData/contributions'; import { @@ -61,9 +61,15 @@ import { } from './mockData/teams'; import { userTasks } from './mockData/tasksStats'; import { homepageStats } from './mockData/homepageStats'; -import { banner, countries, josmRemote, systemStats } from './mockData/miscellaneous'; +import { + banner, + countries, + josmRemote, + systemStats, + ohsomeNowMetadata, +} from './mockData/miscellaneous'; import tasksGeojson from '../../utils/tests/snippets/tasksGeometry'; -import { API_URL } from '../../config'; +import { API_URL, OHSOME_STATS_BASE_URL } from '../../config'; import { notifications, ownCountUnread } from './mockData/notifications'; import { authLogin, setUser, userRegister } from './mockData/auth'; import { @@ -343,21 +349,18 @@ const handlers = [ return res(ctx.json(systemStats)); }), // EXTERNAL API - rest.get('https://osmstats-api.hotosm.org/wildcard', (req, res, ctx) => { + rest.get(`${OHSOME_STATS_BASE_URL}/stats/hotosm-project-%2A`, (req, res, ctx) => { return res(ctx.json(homepageStats)); }), - rest.get( - 'https://osm-stats-production-api.azurewebsites.net/users/:username', - (req, res, ctx) => { - return res(ctx.json(osmStatsProd)); - }, - ), - rest.get( - 'https://osm-stats-production-api.azurewebsites.net/stats/:projectId', - (req, res, ctx) => { - return res(ctx.json(osmStatsProject)); - }, - ), + rest.get(`${OHSOME_STATS_BASE_URL}/hot-tm-user`, (req, res, ctx) => { + return res(ctx.json(ohsomeNowUserStats)); + }), + rest.get(`${OHSOME_STATS_BASE_URL}/stats/:projectId`, (req, res, ctx) => { + return res(ctx.json(osmStatsProject)); + }), + rest.get(`${OHSOME_STATS_BASE_URL}/metadata`, (req, res, ctx) => { + return res(ctx.json(ohsomeNowMetadata)); + }), rest.get('http://127.0.0.1:8111/version', (req, res, ctx) => { return res(ctx.json(josmRemote)); }), diff --git a/frontend/src/utils/internationalization.js b/frontend/src/utils/internationalization.js index fbfbe457b1..cc8e419f15 100644 --- a/frontend/src/utils/internationalization.js +++ b/frontend/src/utils/internationalization.js @@ -131,6 +131,7 @@ let ConnectedIntl = (props) => { locale={props.locale ? props.locale.substr(0, 2) : config.DEFAULT_LOCALE} textComponent={React.Fragment} messages={getTranslatedMessages(props.locale)} + timeZone={Intl.DateTimeFormat().resolvedOptions().timeZone} > {props.children} diff --git a/frontend/src/views/contributions.js b/frontend/src/views/contributions.js index a6e79dda22..fd1b4eaffc 100644 --- a/frontend/src/views/contributions.js +++ b/frontend/src/views/contributions.js @@ -46,7 +46,12 @@ export const ContributionsPage = () => {
- +
); }; diff --git a/frontend/src/views/home.js b/frontend/src/views/home.js index 796ee67f5c..1483a89474 100644 --- a/frontend/src/views/home.js +++ b/frontend/src/views/home.js @@ -9,6 +9,7 @@ import { WhoIsMapping } from '../components/homepage/whoIsMapping'; import { Testimonials } from '../components/homepage/testimonials'; import { Alert } from '../components/alert'; import homeMessages from '../components/homepage/messages'; +import StatsTimestamp from '../components/statsTimestamp/'; export function Home() { return ( @@ -24,6 +25,9 @@ export function Home() { } > +
+ +
diff --git a/frontend/src/views/messages.js b/frontend/src/views/messages.js index 92d046938e..4bfe23c3ef 100644 --- a/frontend/src/views/messages.js +++ b/frontend/src/views/messages.js @@ -33,7 +33,7 @@ export default defineMessages({ defaultMessage: 'Project timeline', }, timelineDataError: { - id: 'project.stats.timeline.fetching.error', + id: 'project.stats.contributions.timeline.fetching.error', defaultMessage: 'An error occured while loading contributions data', }, editsStatsError: { @@ -84,10 +84,6 @@ export default defineMessages({ id: 'management.stats.users.title', defaultMessage: 'New users', }, - totalFeatures: { - id: 'management.stats.features', - defaultMessage: 'Total features', - }, newOrganisation: { id: 'teamsAndOrgs.management.organisation.creation', defaultMessage: 'Create new organization', diff --git a/frontend/src/views/settings.js b/frontend/src/views/settings.js index 4a558c6b05..8aa65032ec 100644 --- a/frontend/src/views/settings.js +++ b/frontend/src/views/settings.js @@ -34,7 +34,7 @@ export function Settings() {
- + {userDetails?.username && }
diff --git a/frontend/src/views/stats.js b/frontend/src/views/stats.js index 309ddaebd0..c98f99541f 100644 --- a/frontend/src/views/stats.js +++ b/frontend/src/views/stats.js @@ -45,9 +45,6 @@ export const Stats = () => {
-

- -

diff --git a/frontend/src/views/teams.js b/frontend/src/views/teams.js index 41c4fd99d2..a11ac3f254 100644 --- a/frontend/src/views/teams.js +++ b/frontend/src/views/teams.js @@ -426,7 +426,7 @@ export function TeamDetail() { const [error, loading, team] = useFetch(`teams/${id}/`); // eslint-disable-next-line const [projectsError, projectsLoading, projects] = useFetch( - `projects/?teamId=${id}&omitMapResults=true`, + `projects/?teamId=${id}&omitMapResults=true&projectStatuses=PUBLISHED,DRAFT,ARCHIVED`, id, ); const [isMember, setIsMember] = useState(false); diff --git a/frontend/src/views/userDetail.js b/frontend/src/views/userDetail.js index 8bb6b5791e..305d508f86 100644 --- a/frontend/src/views/userDetail.js +++ b/frontend/src/views/userDetail.js @@ -12,7 +12,7 @@ import { CountriesMapped } from '../components/userDetail/countriesMapped'; import { TopProjects } from '../components/userDetail/topProjects'; import { ContributionTimeline } from '../components/userDetail/contributionTimeline'; import { NotFound } from './notFound'; -import { USER_STATS_API_URL } from '../config'; +import { OHSOME_STATS_BASE_URL, OSM_SERVER_URL } from '../config'; import { fetchExternalJSONAPI } from '../network/genericJSONRequest'; import { useFetch } from '../hooks/UseFetch'; import { useSetTitleTag } from '../hooks/UseMetaTags'; @@ -33,6 +33,7 @@ export const UserDetail = ({ withHeader = true }) => { const token = useSelector((state) => state.auth.token); const currentUser = useSelector((state) => state.auth.userDetails); const [osmStats, setOsmStats] = useState({}); + const [userOsmDetails, setUserOsmDetails] = useState({}); const [errorDetails, loadingDetails, userDetails] = useFetch( `users/queries/${username}/`, username !== undefined, @@ -53,12 +54,15 @@ export const UserDetail = ({ withHeader = true }) => { }, [navigate, token]); useEffect(() => { - if (token && username) { - fetchExternalJSONAPI(`${USER_STATS_API_URL}${username}`) - .then((res) => setOsmStats(res)) + if (userDetails.id) { + fetchExternalJSONAPI(`${OSM_SERVER_URL}/api/0.6/user/${userDetails.id}.json`, false) + .then((res) => setUserOsmDetails(res?.user)) + .catch((e) => console.log(e)); + fetchExternalJSONAPI(`${OHSOME_STATS_BASE_URL}/hot-tm-user?userId=${userDetails.id}`, true) + .then((res) => setOsmStats(res.result)) .catch((e) => console.log(e)); } - }, [token, username]); + }, [userDetails.id]); const titleClass = 'contributions-titles fw5 ttu barlow-condensed blue-dark mt0'; @@ -74,7 +78,7 @@ export const UserDetail = ({ withHeader = true }) => { rows={5} ready={!errorDetails && !loadingDetails} > - + )} diff --git a/scripts/aws/cloudformation/tasking-manager.template.js b/scripts/aws/cloudformation/tasking-manager.template.js index e03637dc9e..7c66644165 100644 --- a/scripts/aws/cloudformation/tasking-manager.template.js +++ b/scripts/aws/cloudformation/tasking-manager.template.js @@ -562,7 +562,9 @@ const Resources = { Name: cf.stackName, SecurityGroups: [cf.importValue(cf.join('-', ['hotosm-network-production', cf.ref('NetworkEnvironment'), 'elbs-security-group', cf.region]))], Subnets: cf.ref('ELBSubnets'), - Type: 'application' + Type: 'application', + IpAddressType: 'dualstack', + Tags: [ { "Key": "stack_name", "Value": cf.stackName } ] } }, TaskingManagerLoadBalancerRoute53: { @@ -651,6 +653,7 @@ const Resources = { DBInstanceClass: cf.ref('DatabaseInstanceType'), DBSnapshotIdentifier: cf.if('UseASnapshot', cf.ref('DBSnapshot'), cf.noValue), VPCSecurityGroups: [cf.importValue(cf.join('-', ['hotosm-network-production', cf.ref('NetworkEnvironment'), 'ec2s-security-group', cf.region]))], + PubliclyAccessible: false } }, TaskingManagerReactBucket: {