├── .dockerignore ├── .flowconfig ├── .github ├── CODEOWNERS ├── issue_template.md ├── pull_request_template.md └── workflows │ ├── codeql-analysis.yml │ ├── node-ci.yml │ ├── pull-req.yml │ └── push.yml ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .readthedocs.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── __tests__ ├── e2e │ ├── Dockerfile │ ├── docker-compose.yml │ ├── puppeteer │ │ └── Dockerfile │ ├── server │ │ ├── Dockerfile │ │ └── launch.sh │ ├── ui-proxy │ │ ├── Caddyfile │ │ └── Dockerfile │ └── ui │ │ └── Dockerfile ├── end-to-end.js └── test-utils │ ├── e2e-coverage-collector-server.js │ ├── e2e-environment.js │ ├── index.js │ ├── mock-data │ ├── editor.js │ ├── index.js │ ├── manager.js │ └── store.js │ ├── setup-e2e.js │ ├── setup-test-framework.js │ ├── teardown-e2e.js │ ├── travis-client-dev-server.js │ └── utils.js ├── browserstack-logo-600x315.png ├── configurations ├── default │ ├── env.yml.tmp │ └── settings.yml ├── end-to-end │ ├── env.yml.tmp │ ├── test-gtfs-to-fetch.zip │ └── test-gtfs-to-upload.zip └── test │ ├── env.yml │ └── settings.yml ├── docker ├── docker-compose.yml ├── server │ ├── env.yml │ └── server.yml └── ui │ └── Dockerfile ├── docs ├── dev │ ├── api_interaction.md │ ├── deployment.md │ ├── development.md │ ├── localization.md │ └── migration.md ├── index.md ├── style.css └── user │ ├── add-deployment-server.md │ ├── appendix-gtfs-warnings.md │ ├── deploying-feeds.md │ ├── editor │ ├── fares.md │ ├── getting-started.md │ ├── patterns.md │ ├── routes.md │ ├── schedules.md │ └── stops.md │ ├── introduction.md │ ├── managing-projects-feeds.md │ ├── managing-users.md │ ├── otp-deployment.md │ └── setting-up-aws-servers.md ├── flow-typed └── npm │ ├── @conveyal │ └── lonlat_v1.x.x.js │ ├── babel-polyfill_v6.x.x.js │ ├── common-tags_v1.4.x.js │ ├── express_v4.16.x.js │ ├── flow-bin_v0.x.x.js │ ├── isomorphic-fetch_v2.x.x.js │ ├── jest_v22.x.x.js │ ├── js-yaml_v3.x.x.js │ ├── lodash.throttle_v4.x.x.js │ ├── lodash.tolower_v4.x.x.js │ ├── lodash.upperfirst_v4.x.x.js │ ├── lodash_v4.x.x.js │ ├── moment_v2.x.x.js │ ├── nock_v9.x.x.js │ ├── numeral_v2.x.x.js │ ├── object-path_v0.11.x.js │ ├── polyline_v0.2.x.js │ ├── prop-types_v15.x.x.js │ ├── qs_v6.x.x.js │ ├── react-color_v2.x.x.js │ ├── react-dnd-html5-backend_v2.x.x.js │ ├── react-dnd_v2.x.x.js │ ├── react-redux_v5.x.x.js │ ├── redux-actions_v2.x.x.js │ ├── redux_v3.x.x.js │ ├── reselect_v3.x.x.js │ ├── turf-point_v2.x.x.js │ ├── turf-polygon_v1.x.x.js │ ├── uuid_v3.x.x.js │ └── validator_v7.x.x.js ├── gtfs.yml ├── gtfsplus.yml ├── i18n ├── english.yml ├── german.yml └── polish.yml ├── index.html ├── lib ├── admin │ ├── actions │ │ ├── admin.js │ │ └── organizations.js │ ├── components │ │ ├── AccountTypeSelector.js │ │ ├── AdminPage.js │ │ ├── ApplicationStatus.js │ │ ├── CreateUser.js │ │ ├── OrganizationList.js │ │ ├── OrganizationSettings.js │ │ ├── ProjectAccessSettings.js │ │ ├── ServerSettings.js │ │ ├── UserList.js │ │ ├── UserRow.js │ │ ├── UserSettings.js │ │ └── permissions.js │ └── reducers │ │ ├── index.js │ │ ├── organizations.js │ │ ├── servers.js │ │ └── users.js ├── alerts │ ├── actions │ │ ├── activeAlert.js │ │ ├── alerts.js │ │ └── visibilityFilter.js │ ├── components │ │ ├── AffectedEntity.js │ │ ├── AffectedServices.js │ │ ├── AgencySelector.js │ │ ├── AlertEditor.js │ │ ├── AlertPreview.js │ │ ├── AlertsList.js │ │ ├── AlertsViewer.js │ │ ├── CreateAlert.js │ │ ├── ModeSelector.js │ │ ├── RouteSelector.js │ │ └── StopSelector.js │ ├── containers │ │ ├── ActiveAlertEditor.js │ │ ├── MainAlertsViewer.js │ │ └── VisibleAlertsList.js │ ├── reducers │ │ ├── active.js │ │ ├── alerts.js │ │ └── index.js │ ├── selectors │ │ └── index.js │ └── util │ │ └── index.js ├── assets │ ├── application_icon.png │ └── application_logo.png ├── common │ ├── actions │ │ └── index.js │ ├── components │ │ ├── ClickOutside.js │ │ ├── ConfirmModal.js │ │ ├── EC2InstanceCard.js │ │ ├── EditableTextField.js │ │ ├── ExportPatternsModal.js │ │ ├── FeedLabel.js │ │ ├── FormInput.js │ │ ├── InfoModal.js │ │ ├── JobMonitor.js │ │ ├── LanguageSelect.js │ │ ├── Loading.js │ │ ├── Login.js │ │ ├── ManagerPage.js │ │ ├── MapModal.js │ │ ├── MenuItem.js │ │ ├── OptionButton.js │ │ ├── PageNotFound.js │ │ ├── SelectFileModal.js │ │ ├── Sidebar.js │ │ ├── SidebarNavItem.js │ │ ├── SidebarPopover.js │ │ ├── StatusMessage.js │ │ ├── StatusModal.js │ │ ├── TimezoneSelect.js │ │ ├── Title.js │ │ └── UserButtons.js │ ├── constants │ │ └── index.js │ ├── containers │ │ ├── ActiveSidebar.js │ │ ├── ActiveSidebarNavItem.js │ │ ├── ActiveUserRetriever.js │ │ ├── App.js │ │ ├── AppInfoRetriever.js │ │ ├── CurrentStatusMessage.js │ │ ├── CurrentStatusModal.js │ │ ├── LocalUserRetriever.js │ │ ├── PageContent.js │ │ ├── StarButton.js │ │ ├── WatchButton.js │ │ └── wrapComponentInAuthStrategy.js │ ├── user │ │ ├── UserPermissions.js │ │ └── UserSubscriptions.js │ └── util │ │ ├── __tests__ │ │ ├── config.js │ │ ├── gtfs.js │ │ ├── map.js │ │ └── permissions.js │ │ ├── analytics.js │ │ ├── config.js │ │ ├── date-time.js │ │ ├── exceptions.js │ │ ├── file-download.js │ │ ├── geo.js │ │ ├── gtfs.js │ │ ├── json.js │ │ ├── map-keys.js │ │ ├── maps.js │ │ ├── modules.js │ │ ├── permissions.js │ │ ├── text.js │ │ ├── timezones.js │ │ ├── upload-file.js │ │ ├── user.js │ │ └── util.js ├── editor │ ├── actions │ │ ├── active.js │ │ ├── editor.js │ │ ├── map │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── stopStrategies.js.snap │ │ │ │ ├── fixtures │ │ │ │ │ ├── loop-ctrl-points.json │ │ │ │ │ ├── loop-pattern-segments.json │ │ │ │ │ └── loop-pattern-stops.json │ │ │ │ └── stopStrategies.js │ │ │ ├── index.js │ │ │ └── stopStrategies.js │ │ ├── snapshots.js │ │ ├── trip.js │ │ └── tripPattern.js │ ├── components │ │ ├── BulkEditorModal.js │ │ ├── ColorField.js │ │ ├── CreateSnapshotModal.js │ │ ├── EditorFeedSourcePanel.js │ │ ├── EditorHelpModal.js │ │ ├── EditorInput.js │ │ ├── EditorSidebar.js │ │ ├── EntityDetails.js │ │ ├── EntityDetailsHeader.js │ │ ├── EntityList.js │ │ ├── EntityListButtons.js │ │ ├── EntityListSecondaryActions.js │ │ ├── ExceptionDate.js │ │ ├── ExceptionDateRange.js │ │ ├── ExceptionValidationErrorsList.js │ │ ├── FareRuleSelections.js │ │ ├── FareRulesForm.js │ │ ├── FeedInfoPanel.js │ │ ├── GtfsEditor.js │ │ ├── HourMinuteInput.js │ │ ├── MinuteSecondInput.js │ │ ├── RouteTypeSelect.js │ │ ├── ScheduleExceptionForm.js │ │ ├── VirtualizedEntitySelect.js │ │ ├── ZoneSelect.js │ │ ├── map │ │ │ ├── AddableStop.js │ │ │ ├── AddableStopsLayer.js │ │ │ ├── ControlPoint.js │ │ │ ├── ControlPointsLayer.js │ │ │ ├── DirectionIconsLayer.js │ │ │ ├── EditorMap.js │ │ │ ├── EditorMapLayersControl.js │ │ │ ├── PatternStopMarker.js │ │ │ ├── PatternStopsLayer.js │ │ │ ├── PatternsLayer.js │ │ │ ├── StopsLayer.js │ │ │ ├── TextPath.js │ │ │ └── pattern-debug-lines.js │ │ ├── pattern │ │ │ ├── AddPatternStopDropdown.js │ │ │ ├── CalculateDefaultTimesForm.js │ │ │ ├── EditSchedulePanel.js │ │ │ ├── EditSettings.js │ │ │ ├── EditShapePanel.js │ │ │ ├── NormalizeStopTimesModal.js │ │ │ ├── NormalizeStopTimesTip.js │ │ │ ├── PatternStopButtons.js │ │ │ ├── PatternStopCard.js │ │ │ ├── PatternStopContainer.js │ │ │ ├── PatternStopsPanel.js │ │ │ ├── TripPatternList.js │ │ │ ├── TripPatternListControls.js │ │ │ └── TripPatternViewer.js │ │ └── timetable │ │ │ ├── CalendarSelect.js │ │ │ ├── EditableCell.js │ │ │ ├── HeaderCell.js │ │ │ ├── PatternSelect.js │ │ │ ├── RouteSelect.js │ │ │ ├── Timetable.js │ │ │ ├── TimetableEditor.js │ │ │ ├── TimetableGrid.js │ │ │ ├── TimetableHeader.js │ │ │ ├── TimetableHelpModal.js │ │ │ └── TripSeriesModal.js │ ├── constants │ │ └── index.js │ ├── containers │ │ ├── ActiveEditorFeedSourcePanel.js │ │ ├── ActiveEditorLockManager.js │ │ ├── ActiveEntityList.js │ │ ├── ActiveFeedInfoPanel.js │ │ ├── ActiveGtfsEditor.js │ │ ├── ActiveTimetableEditor.js │ │ └── ActiveTripPatternList.js │ ├── reducers │ │ ├── data.js │ │ ├── index.js │ │ ├── mapState.js │ │ ├── settings.js │ │ └── timetable.js │ ├── selectors │ │ ├── index.js │ │ └── timetable.js │ └── util │ │ ├── __tests__ │ │ ├── __snapshots__ │ │ │ └── map.js.snap │ │ ├── fixtures │ │ │ ├── graphhopper-response-delete-middle-control-point.json │ │ │ ├── graphhopper-response-update-first-stop.json │ │ │ ├── graphhopper-response-update-last-stop.json │ │ │ ├── graphhopper-response-update-middle-control-point.json │ │ │ ├── test-control-points-with-extra-point-at-end.json │ │ │ ├── test-control-points.json │ │ │ └── test-pattern-shape.json │ │ ├── gtfs.js │ │ ├── index.js │ │ ├── map.js │ │ └── validation.js │ │ ├── debug.js │ │ ├── gtfs.js │ │ ├── index.js │ │ ├── map.js │ │ ├── objects.js │ │ ├── timetable.js │ │ ├── types.js │ │ ├── ui.js │ │ └── validation.js ├── gtfs │ ├── actions │ │ ├── filter.js │ │ ├── general.js │ │ ├── patterns.js │ │ ├── routes.js │ │ ├── shapes.js │ │ └── timetables.js │ ├── components │ │ ├── GtfsFilter.js │ │ ├── GtfsMap.js │ │ ├── PatternGeoJson.js │ │ ├── ShowAllRoutesOnMapFilter.js │ │ ├── StopMarker.js │ │ ├── TransferPerformance.js │ │ ├── gtfs-search.js │ │ └── gtfsmapsearch.js │ ├── containers │ │ ├── ActiveGtfsMap.js │ │ ├── GlobalGtfsFilter.js │ │ └── ShowAllRoutesOnMapFilter.js │ ├── reducers │ │ ├── filter.js │ │ ├── index.js │ │ ├── patterns.js │ │ ├── routes.js │ │ ├── shapes.js │ │ ├── stops.js │ │ ├── timetables.js │ │ └── validation.js │ ├── selectors │ │ └── index.js │ └── util │ │ ├── __tests__ │ │ └── stats.js │ │ ├── graphql.js │ │ ├── index.js │ │ └── stats.js ├── gtfsplus │ ├── actions │ │ └── gtfsplus.js │ ├── components │ │ ├── GtfsPlusEditor.js │ │ ├── GtfsPlusField.js │ │ ├── GtfsPlusFieldHeader.js │ │ ├── GtfsPlusTable.js │ │ └── GtfsPlusVersionSummary.js │ ├── containers │ │ ├── ActiveGtfsPlusEditor.js │ │ └── ActiveGtfsPlusVersionSummary.js │ ├── reducers │ │ ├── gtfsplus.js │ │ └── index.js │ ├── selectors │ │ └── index.js │ └── util │ │ └── index.js ├── index.css ├── main.js ├── manager │ ├── actions │ │ ├── __tests__ │ │ │ ├── __snapshots__ │ │ │ │ ├── projects.js.snap │ │ │ │ └── user.js.snap.hold │ │ │ ├── projects.js │ │ │ └── user.js.hold │ │ ├── deployments.js │ │ ├── feeds.js │ │ ├── labels.js │ │ ├── languages.js │ │ ├── notes.js │ │ ├── projects.js │ │ ├── status.js │ │ ├── user.js │ │ ├── versions.js │ │ └── visibilityFilter.js │ ├── components │ │ ├── AutoPublishSettings.js │ │ ├── CollapsiblePanel.js │ │ ├── CreateFeedSource.js │ │ ├── CreateProject.js │ │ ├── ExternalPropertiesTable.js │ │ ├── FeedFetchFrequency.js │ │ ├── FeedSourcePanel.js │ │ ├── FeedSourceSettings.js │ │ ├── FeedSourceTable.js │ │ ├── FeedSourceTableRow.js │ │ ├── FeedSourceViewer.js │ │ ├── GeneralSettings.js │ │ ├── GtfsFieldSelector.js │ │ ├── HomeProjectDropdown.js │ │ ├── LabelAssigner.js │ │ ├── LabelAssignerModal.js │ │ ├── LabelEditor.js │ │ ├── LabelEditorModal.js │ │ ├── LabelPanel.js │ │ ├── ManagerHeader.js │ │ ├── NoteForm.js │ │ ├── NotesViewer.js │ │ ├── ProjectFeedListToolbar.js │ │ ├── ProjectSettings.js │ │ ├── ProjectSettingsForm.js │ │ ├── ProjectViewer.js │ │ ├── ProjectsList.js │ │ ├── RecentActivityBlock.js │ │ ├── TransformationsViewer.js │ │ ├── UserAccountInfoPanel.js │ │ ├── UserHomePage.js │ │ ├── deployment │ │ │ ├── CurrentDeploymentPanel.js │ │ │ ├── CustomConfig.js │ │ │ ├── CustomFileEditor.js │ │ │ ├── DeploymentConfigurationsPanel.js │ │ │ ├── DeploymentConfirmModal.js │ │ │ ├── DeploymentPreviewButton.js │ │ │ ├── DeploymentTableRow.js │ │ │ ├── DeploymentVersionsTable.js │ │ │ ├── DeploymentViewer.js │ │ │ ├── DeploymentsPanel.js │ │ │ └── PeliasPanel.js │ │ ├── reporter │ │ │ ├── components │ │ │ │ ├── DateTimeFilter.js │ │ │ │ ├── PatternLayout.js │ │ │ │ ├── RouteLayout.js │ │ │ │ ├── StopLayout.js │ │ │ │ ├── TimetableLayout.js │ │ │ │ └── TripsPerHourChart.js │ │ │ └── containers │ │ │ │ ├── ActiveDateTimeFilter.js │ │ │ │ ├── Patterns.js │ │ │ │ ├── Routes.js │ │ │ │ ├── Stops.js │ │ │ │ └── Timetables.js │ │ ├── transform │ │ │ ├── AddCustomFile.js │ │ │ ├── CustomCSVForm.js │ │ │ ├── FeedTransformRules.js │ │ │ ├── FeedTransformation.js │ │ │ ├── FeedTransformationSettings.js │ │ │ ├── NormalizeField.js │ │ │ ├── PreserveCustomFields.js │ │ │ ├── ReplaceFileFromString.js │ │ │ ├── ReplaceFileFromVersion.js │ │ │ ├── SubstitutionRow.js │ │ │ └── TransformationsIndicatorBadge.js │ │ ├── validation │ │ │ ├── GtfsValidationViewer.js │ │ │ ├── MobilityDataValidationResult.js │ │ │ ├── ServicePerModeChart.js │ │ │ ├── TripsChart.js │ │ │ ├── ValidationErrorItem.js │ │ │ └── rules.json │ │ └── version │ │ │ ├── DeltaStat.js │ │ │ ├── FeedVersionAccessibility.js │ │ │ ├── FeedVersionDetails.js │ │ │ ├── FeedVersionMap.js │ │ │ ├── FeedVersionNavigator.js │ │ │ ├── FeedVersionReport.js │ │ │ ├── FeedVersionSpanChart.js │ │ │ ├── FeedVersionTabs.js │ │ │ ├── FeedVersionViewer.js │ │ │ ├── VersionButtonToolbar.js │ │ │ ├── VersionComparisonDropdown.js │ │ │ ├── VersionDateLabel.js │ │ │ ├── VersionRetrievalBadge.js │ │ │ └── VersionSelectorDropdown.js │ ├── containers │ │ ├── ActiveDeploymentViewer.js │ │ ├── ActiveFeedSourceViewer.js │ │ ├── ActiveFeedVersionNavigator.js │ │ ├── ActiveProjectViewer.js │ │ ├── ActiveProjectsList.js │ │ ├── ActiveUserHomePage.js │ │ ├── CreateProject.js │ │ ├── DeploymentsPanel.js │ │ ├── FeedSourceTable.js │ │ ├── FeedSourceTableRow.js │ │ ├── ProjectFeedListToolbar.js │ │ └── __tests__ │ │ │ ├── ActiveProjectViewer.js │ │ │ ├── DeploymentsPanel.js │ │ │ ├── FeedSourceTable.js │ │ │ └── __snapshots__ │ │ │ ├── ActiveProjectViewer.js.snap │ │ │ ├── DeploymentsPanel.js.snap │ │ │ └── FeedSourceTable.js.snap │ ├── reducers │ │ ├── index.js │ │ ├── languages.js │ │ ├── projects.js │ │ ├── status.js │ │ ├── ui.js │ │ └── user.js │ ├── selectors │ │ ├── __tests__ │ │ │ ├── __snapshots__ │ │ │ │ └── index.js.snap │ │ │ └── index.js │ │ └── index.js │ └── util │ │ ├── __tests__ │ │ ├── __snapshots__ │ │ │ ├── index.js.snap │ │ │ └── transform.js.snap │ │ ├── index.js │ │ └── transform.js │ │ ├── deployment.js │ │ ├── enums │ │ └── transform.js │ │ ├── index.js │ │ ├── transform.js │ │ ├── validation.js │ │ └── version.js ├── public │ ├── components │ │ ├── LicenseTerms.js │ │ ├── PublicHeader.js │ │ ├── PublicLandingPage.js │ │ ├── PublicPage.js │ │ └── UserAccount.js │ └── containers │ │ ├── ActiveLicenseTerms.js │ │ ├── ActivePublicHeader.js │ │ ├── ActivePublicLandingPage.js │ │ └── ActiveUserAccount.js ├── scenario-editor │ ├── components │ │ └── StopLayer.js │ └── utils │ │ ├── reverse.js │ │ └── valhalla.js ├── style.css └── types │ ├── actions.js │ ├── index.js │ └── reducers.js ├── mkdocs.yml ├── package.json ├── patches └── .gitkeep ├── scripts ├── lint-messages.js ├── load.py ├── loadLegacy.py ├── migrate-auth0-users.js ├── package.json ├── seedData.js ├── updateAppMetadata.js └── yarn.lock └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .git/ 3 | .DS_Store -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/node_modules/config-chain/.* 3 | .*/node_modules/fbjs/flow/.* 4 | .*/node_modules/immutable/.* 5 | .*/node_modules/jju/examples.* 6 | .*/node_modules/mapbox.js/docs/examples/.* 7 | .*/node_modules/nock/node_modules/changelog/examples/.* 8 | .*/node_modules/npmconf/.* 9 | .*/node_modules/react-leaflet/src/.* 10 | .*/node_modules/react-leaflet/lib/*.*\.js\.flow 11 | .*/node_modules/reqwest/.* 12 | .*/node_modules/module-deps/test/invalid_pkg/package.json 13 | .*/node_modules/immutable/dist/immutable.js.flow 14 | 15 | [include] 16 | 17 | [libs] 18 | 19 | [options] 20 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/about-codeowners/ 2 | 3 | # An Arcadis OTP/TRANSIT-data-tools member is required to approve PR merges 4 | * @ibi-group/otp-data-tools 5 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | _**NOTE:** This issue system is intended for reporting bugs and tracking progress in software development. Although this software is licensed with an open-source license, any issue opened here may not be dealt with in a timely manner. [Arcadis IBI Group](https://www.ibigroup.com/) is able to provide technical support for custom deployments of this software. Please contact [Jon Campbell](mailto:jon.campbell@ibigroup.com?subject=Data%20Tools%20inquiry%20via%20GitHub&body=Name:%20%0D%0AAgency/Company:%20%0D%0ABest%20date/time%20for%20a%20demo/discussion:%20%0D%0ADescription%20of%20needs:%20) if your company or organization is interested in opening a support contract with us. Please remove this note when creating the issue._ 2 | 3 | ## Observed behavior (please include a screenshot if possible) 4 | 5 | Please explain what is being observed within the application here. 6 | 7 | ## Expected behavior 8 | 9 | Please explain what should happen instead. 10 | 11 | ## Steps to reproduce the problem 12 | 13 | Please be as specific as possible. 14 | 15 | ## Any special notes on configuration used 16 | 17 | Please describe any applicable config files that were used 18 | 19 | ## Version of datatools-ui and datatools-server if applicable (exact commit hash or branch name) 20 | 21 | This info can be found by clicking on the gear icon on the sidebar 22 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### Checklist 2 | 3 | - [ ] Appropriate branch selected _(all PRs must first be merged to `dev` before they can be merged to `master`)_ 4 | - [ ] Any modified or new methods or classes have helpful JSDoc and code is thoroughly commented 5 | - [ ] The description lists all applicable issues this PR seeks to resolve 6 | - [ ] The description lists any configuration setting(s) that differ from the default settings 7 | - [ ] All tests and CI builds passing 8 | - [ ] The description lists all relevant PRs included in this release _(remove this if not merging to master)_ 9 | - [ ] e2e tests are all passing _(remove this if not merging to master)_ 10 | 11 | ### Description 12 | 13 | Please explain the changes you made here and, if not immediately obvious from the code, how they resolve any referenced issues (and PRs if merging to master). Be sure to include all issues being resolved and any special configuration settings that are need for the software to run properly with these changes. 14 | -------------------------------------------------------------------------------- /.github/workflows/pull-req.yml: -------------------------------------------------------------------------------- 1 | name: PR E2E Checks 2 | 3 | on: 4 | pull_request: 5 | types: [opened, synchronize, reopened, ready_for_review] 6 | jobs: 7 | pr-e2e-check: 8 | # Run e2e tests: 9 | # - on pull request changed from draft to ready-for-review 10 | # - on commits to: 11 | # * PRs to master 12 | # * PRs to dev that are not draft 13 | # * PRs to dev on branches created by dependabot 14 | if: "${{ 15 | github.event_name == 'pull_request' && ( 16 | github.event.action == 'review_requested' || 17 | github.base_ref == 'master' || 18 | (github.base_ref == 'dev' && ( 19 | github.event.pull_request.draft == false || 20 | startsWith(github.head_ref, 'dependabot/') 21 | )) 22 | ) 23 | }}" 24 | uses: ./.github/workflows/node-ci.yml 25 | with: 26 | e2e: true 27 | secrets: inherit 28 | -------------------------------------------------------------------------------- /.github/workflows/push.yml: -------------------------------------------------------------------------------- 1 | name: Commit Checks 2 | 3 | on: [push] 4 | 5 | jobs: 6 | push-checks: 7 | uses: ./.github/workflows/node-ci.yml 8 | with: 9 | # Run e2e tests on pushes to dev and master. 10 | e2e: ${{ github.ref_name == 'dev' || github.ref_name == 'master' }} 11 | secrets: inherit 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .DS_Store 3 | dist 4 | *.log 5 | coverage* 6 | e2e-test-results 7 | tmp/ 8 | .tags 9 | *.pid 10 | 11 | # Ignore mobility data jar for building rules 12 | *.jar 13 | 14 | # Optional npm cache directory 15 | .npm 16 | 17 | # Optional REPL history 18 | .node_repl_history 19 | 20 | # Configurations 21 | configurations/* 22 | !configurations/default 23 | !configurations/test 24 | !configurations/end-to-end 25 | dist 26 | assets 27 | 28 | # Secret config files 29 | env.yml 30 | env.yml-original 31 | .env 32 | !configurations/test/env.yml 33 | !docker/server/env.yml 34 | scripts/*client.json 35 | *.pem 36 | 37 | # Vs code settings 38 | .vscode/ 39 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file for MkDocs projects 2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | 4 | # Required 5 | version: 2 6 | 7 | # Set the version of Python and other tools you might need 8 | build: 9 | os: ubuntu-22.04 10 | tools: 11 | python: "3.11" 12 | 13 | mkdocs: 14 | configuration: mkdocs.yml 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | ---------------------- 3 | ## Next 4 | * Support German language and add initial translations. 5 | ## Former 6 | * Fix timetable previous stop time checks from checking text columns. 7 | * Add feature allowing routing to avoid highways. 8 | * Support Polish language and add initial translations. 9 | * Fix bug displaying null continuous_pickup as '0'. 10 | * Add route type selector with new extended GTFS route types. 11 | * Fix bug to update continuous_pickup/dropoff values correctly. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Conveyal 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # datatools-ui 2 | 3 | [![Join the chat at https://matrix.to/#/#transit-data-tools:gitter.im](https://badges.gitter.im/repo.png)](https://matrix.to/#/#transit-data-tools:gitter.im) 4 | 5 | The core application for IBI Group's TRANSIT-Data-Tools suite. This application provides GTFS editing, management, validation, and deployment to OpenTripPlanner. 6 | 7 | ## Quick Start 8 | 9 | A pre-configured datatools instance can be lauched via Docker by running 10 | 11 | ```bash 12 | cd docker 13 | cp ../configurations/default/env.yml.tmp ../configurations/default/env.yml 14 | docker compose up 15 | ``` 16 | 17 | from the datatools-ui directory. Datatools will then be running on port `9966`. 18 | 19 | Deployment functionality will not work, and persistence may only work in certain cases (look into Docker volumes for more info). 20 | 21 | ## Configuration 22 | 23 | This repository serves as the front end UI for the Data Manager application. It must be run in conjunction with [datatools-server](https://github.com/conveyal/datatools-server) 24 | 25 | ## Documentation 26 | 27 | View the [latest release documentation](http://data-tools-docs.ibi-transit.com/en/latest/) at ReadTheDocs for more info on deployment and development as well as a user guide. 28 | 29 | Note: `dev` branch docs (which refer to the default `branch` and are more up-to-date and accurate for most users) can be found [here](http://data-tools-docs.ibi-transit.com/en/dev/). 30 | 31 | ## Getting in touch 32 | 33 | We have a Gitter [space](https://matrix.to/#/#transit-data-tools:gitter.im) for the full TRANSIT-Data-Tools project where you can post questions and comments. 34 | 35 | ## Shoutouts 🙏 36 | 37 | BrowserStack Logo 38 | 39 | Big thanks to [BrowserStack](https://www.browserstack.com) for letting the maintainers use their service to debug browser issues. 40 | 41 | GraphHopper Logo 42 | 43 | Street snapping powered by the GraphHopper API. 44 | -------------------------------------------------------------------------------- /__tests__/e2e/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | FROM public.ecr.aws/s2a5w2n9/puppeteer:latest 3 | WORKDIR /datatools-ui 4 | 5 | USER root 6 | RUN apk add --no-cache git 7 | 8 | RUN yarn global add https://github.com/ibi-group/otp-runner.git 9 | RUN yarn global add miles-grant-ibigroup/mastarm#f61ca541a788e8cae8a0e32b886de754846ea16f 10 | 11 | COPY package.json yarn.lock /datatools-ui/ 12 | RUN yarn 13 | COPY . /datatools-ui/ 14 | 15 | RUN mkdir -p /opt/otp 16 | RUN mkdir -p /datatools-ui/e2e-test-results/ 17 | RUN mkdir ~/.aws && printf '%s\n' '[default]' 'aws_access_key_id=${AWS_ACCESS_KEY_ID}' 'aws_secret_access_key=${AWS_SECRET_ACCESS_KEY}' 'region=${AWS_REGION}' > ~/.aws/config 18 | 19 | RUN wget https://raw.githubusercontent.com/ettore26/wait-for-command/master/wait-for-command.sh 20 | RUN chmod +x ./wait-for-command.sh 21 | 22 | ENV TEST_FOLDER_PATH=/datatools-ui/e2e-test-results 23 | ENV IS_DOCKER=true -------------------------------------------------------------------------------- /__tests__/e2e/puppeteer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine 2 | 3 | # Installs latest Chromium (100) package. 4 | RUN apk add --no-cache \ 5 | chromium \ 6 | nss \ 7 | freetype \ 8 | harfbuzz \ 9 | ca-certificates \ 10 | ttf-freefont \ 11 | nodejs \ 12 | yarn 13 | 14 | # Tell Puppeteer to skip installing Chrome. We'll be using the installed package. 15 | ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true \ 16 | PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser 17 | 18 | # Puppeteer v13.5.0 works with Chromium 100. 19 | RUN yarn add puppeteer 20 | 21 | # Add user so we don't need --no-sandbox. 22 | RUN addgroup -S pptruser && adduser -S -G pptruser pptruser \ 23 | && mkdir -p /home/pptruser/Downloads /app \ 24 | && chown -R pptruser:pptruser /home/pptruser \ 25 | && chown -R pptruser:pptruser /app 26 | 27 | # Run everything after as non-privileged user. 28 | USER pptruser -------------------------------------------------------------------------------- /__tests__/e2e/server/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | FROM maven:3.8.7-openjdk-18 3 | 4 | WORKDIR /datatools 5 | 6 | ARG E2E_AUTH0_USERNAME 7 | ARG E2E_AUTH0_PASSWORD 8 | ARG E2E_S3_BUCKET 9 | ARG MS_TEAMS_WEBHOOK_URL 10 | ARG GITHUB_REF_SLUG 11 | ARG GITHUB_SHA 12 | ARG TRANSITFEEDS_KEY 13 | ARG GITHUB_REPOSITORY 14 | ARG GITHUB_WORKSPACE 15 | ARG GITHUB_RUN_ID 16 | ARG AUTH0_CLIENT_ID 17 | ARG AUTH0_PUBLIC_KEY 18 | ARG AUTH0_DOMAIN 19 | ARG AUTH0_CONNECTION_NAME 20 | ARG AUTH0_API_CLIENT 21 | ARG AUTH0_API_SECRET 22 | ARG OSM_VEX 23 | ARG SPARKPOST_KEY 24 | ARG SPARKPOST_EMAIL 25 | ARG GTFS_DATABASE_URL 26 | ARG GTFS_DATABASE_USER 27 | ARG GTFS_DATABASE_PASSWORD 28 | ARG MONGO_DB_NAME 29 | ARG MONGO_HOST 30 | ARG AWS_ACCESS_KEY_ID 31 | ARG AWS_REGION 32 | ARG AWS_SECRET_ACCESS_KEY 33 | 34 | # Grab latest dev build of Datatools Server 35 | RUN git clone https://github.com/ibi-group/datatools-server.git 36 | RUN microdnf install wget 37 | WORKDIR /datatools/datatools-server 38 | 39 | RUN mvn package -DskipTests 40 | RUN cp target/dt*.jar ./datatools-server-3.8.1-SNAPSHOT.jar 41 | 42 | # Grab latest dev build of OTP 43 | RUN wget https://repo1.maven.org/maven2/org/opentripplanner/otp/1.4.0/otp-1.4.0-shaded.jar 44 | RUN mkdir -p /tmp/otp/graphs 45 | RUN mkdir -p /var/datatools_gtfs 46 | 47 | RUN mkdir ~/.aws && printf '%s\n' '[default]' 'aws_access_key_id=${AWS_ACCESS_KEY_ID}' 'aws_secret_access_key=${AWS_SECRET_ACCESS_KEY}' 'region=${AWS_REGION}' > ~/.aws/config 48 | 49 | # Grab server config 50 | RUN mkdir /config 51 | RUN wget https://raw.githubusercontent.com/ibi-group/datatools-server/dev/configurations/default/server.yml.tmp -O /config/server.yml 52 | 53 | # The enviornment variables contain everything needed on the server 54 | COPY __tests__/e2e/server/datatools.pem /datatools/ 55 | RUN touch /config/env.yml 56 | RUN env | sed 's/\=/\: /' > /config/env.yml 57 | 58 | COPY __tests__/e2e/server/launch.sh launch.sh 59 | RUN chmod +x launch.sh 60 | CMD ./launch.sh 61 | EXPOSE 8080 62 | EXPOSE 4000 63 | -------------------------------------------------------------------------------- /__tests__/e2e/server/launch.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Start the first process 4 | java -jar otp-1.4.0-shaded.jar --server --autoScan --basePath /tmp/otp --insecure --router default & 5 | 6 | # Start the second process 7 | java -jar datatools-server-3.8.1-SNAPSHOT.jar /config/env.yml /config/server.yml & 8 | 9 | # Wait for any process to exit 10 | wait -n 11 | 12 | # Exit with status of process that exited first 13 | exit $? -------------------------------------------------------------------------------- /__tests__/e2e/ui-proxy/Caddyfile: -------------------------------------------------------------------------------- 1 | datatools-ui-proxy { 2 | reverse_proxy datatools-ui:9966 3 | tls internal 4 | } 5 | -------------------------------------------------------------------------------- /__tests__/e2e/ui-proxy/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM caddy:latest 2 | COPY ./__tests__/e2e/ui-proxy/Caddyfile /etc/caddy/Caddyfile 3 | EXPOSE 443 -------------------------------------------------------------------------------- /__tests__/e2e/ui/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | FROM node:14 3 | WORKDIR /datatools-build 4 | 5 | ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD true 6 | ARG BUGSNAG_KEY 7 | 8 | RUN cd /datatools-build 9 | COPY package.json yarn.lock patches /datatools-build/ 10 | RUN yarn 11 | COPY . /datatools-build/ 12 | COPY configurations/default /datatools-config/ 13 | CMD yarn run mastarm build --env dev --serve --proxy http://datatools-server:4000/api # -------------------------------------------------------------------------------- /__tests__/test-utils/e2e-coverage-collector-server.js: -------------------------------------------------------------------------------- 1 | // A simple server used to collect and aggregate code coverage from the e2e 2 | // tests. It also allows a coverage report to be downloaded. 3 | // copied from https://github.com/ORESoftware/express-istanbul 4 | 5 | const cov = require('istanbul-middleware') 6 | const express = require('express') 7 | const app = express() 8 | 9 | app.use('/coverage', cov.createHandler()) 10 | 11 | app.listen(9999) 12 | -------------------------------------------------------------------------------- /__tests__/test-utils/e2e-environment.js: -------------------------------------------------------------------------------- 1 | const NodeEnvironment = require('jest-environment-node') 2 | 3 | /** 4 | * This class does 2 major things. 5 | * 6 | * 1) It runs the setup scripts before the end-to-end test script is ran. 7 | * 2) It runs the teardown scripts after the end-to-end test script is ran. 8 | * 9 | * It is much cleaner to do these steps in here because the setup and teardown 10 | * scripts take a while to complete and are setting up the overall environment 11 | * for the e2e tests which when ran in a CI environment require the starting 12 | * of various servers. Also, even if ran locally, collecting coverage requires 13 | * starting a server as well. 14 | */ 15 | class EndToEndEnvironment extends NodeEnvironment { 16 | async setup () { 17 | await require('./setup-e2e')() 18 | await super.setup() 19 | console.log('finished setting up EndToEndEnvironment') 20 | } 21 | 22 | async teardown () { 23 | await require('./teardown-e2e')() 24 | await super.teardown() 25 | } 26 | 27 | runScript (script) { 28 | return super.runScript(script) 29 | } 30 | } 31 | 32 | module.exports = EndToEndEnvironment 33 | -------------------------------------------------------------------------------- /__tests__/test-utils/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | /** 4 | * Temporarily change the internal behavior of the Date.now method such that it 5 | * returns a time that is based off of the given value in milliseconds after 6 | * the epoch. Typically the method Date.UTC(YYYY, MM, DD) can be used to 7 | * generate this number. 8 | */ 9 | export function setTestTime (time: number) { 10 | jest.spyOn(Date, 'now').mockImplementation(() => new Date(time).valueOf()) 11 | } 12 | 13 | /** 14 | * Sets the default mock test time for a variety of tests such that various 15 | * calculations and feed version statuses resolve to a certain state. 16 | */ 17 | export function setDefaultTestTime () { 18 | setTestTime(Date.UTC(2019, 4, 22)) 19 | } 20 | 21 | /** 22 | * Restore the standard functionality of Date library. This should be used in 23 | * the afterEach clause in test suites that require a mocked date. 24 | */ 25 | export function restoreDateNowBehavior () { 26 | Date.now.mockRestore && Date.now.mockRestore() 27 | } 28 | 29 | export function expectArrayToMatchContents (actual: any, expected: Array) { 30 | expect(actual).toHaveLength(expected.length) 31 | expect(actual).toEqual(expect.arrayContaining(expected)) 32 | } 33 | -------------------------------------------------------------------------------- /__tests__/test-utils/mock-data/editor.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export const mockTripWithoutStopTimes = { 4 | bikes_allowed: null, 5 | blockId: '1', 6 | directionId: 0, 7 | frequencies: [], 8 | id: 3, 9 | pattern_id: '1', 10 | route_id: 'GL', 11 | service_id: 'GL-1', 12 | shape_id: '19', 13 | stopTimes: [], 14 | tripHeadsign: 'Walla Walla', 15 | stopHeadsign: 'North', 16 | tripId: 'test-trip-id-1', 17 | tripShortName: null, 18 | wheelchair_accessible: null 19 | } 20 | -------------------------------------------------------------------------------- /__tests__/test-utils/mock-data/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import * as manager from './manager' 4 | import * as store from './store' 5 | 6 | export default { 7 | manager, 8 | store 9 | } 10 | -------------------------------------------------------------------------------- /__tests__/test-utils/setup-test-framework.js: -------------------------------------------------------------------------------- 1 | // This file is ran before each test within the testing environemnt. This is 2 | // needed when running with units tests because localStorage isn't implemented 3 | // in an expected way within jsdom. This test also runs during the e2e tests, 4 | // so we need to make sure the window variable is present. 5 | 6 | if (typeof window !== 'undefined') { 7 | window.localStorage = { 8 | getItem: () => null 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /__tests__/test-utils/travis-client-dev-server.js: -------------------------------------------------------------------------------- 1 | // Travis was getting some errors from budo because it was watching lots of 2 | // files. This server is just to serve up the built files and proxy some 3 | // requests to datatools-server. It also helps because the build script can 4 | // be ran and tracked independently of this server. 5 | 6 | const path = require('path') 7 | 8 | const express = require('express') 9 | const proxy = require('express-http-proxy') 10 | const app = express() 11 | 12 | app.use('/dist', express.static('dist')) 13 | app.use( 14 | '/api', 15 | proxy( 16 | 'http://localhost:4000/api/', 17 | { 18 | proxyReqPathResolver: req => { 19 | // need to rewrite the url to include the api part 20 | return `/api${req.url}` 21 | } 22 | } 23 | ) 24 | ) 25 | app.get('/*', (req, res) => { 26 | res.sendFile(path.resolve(__dirname, '../../index.html')) 27 | }) 28 | 29 | const port = 9966 30 | app.listen(port, () => { 31 | console.log('server listening on port ' + port) 32 | }) 33 | -------------------------------------------------------------------------------- /browserstack-logo-600x315.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibi-group/datatools-ui/70adc13a79cdfe8c2c6eadafbe2a581a375d26ed/browserstack-logo-600x315.png -------------------------------------------------------------------------------- /configurations/default/env.yml.tmp: -------------------------------------------------------------------------------- 1 | AUTH0_CLIENT_ID: your-auth0-client-id 2 | AUTH0_CONNECTION_NAME: your-auth0-connection-name 3 | AUTH0_DOMAIN: your-auth0-domain 4 | BUGSNAG_KEY: optional-bugsnag-key 5 | MAP_BASE_URL: optional-map-tile-url 6 | MAPBOX_ACCESS_TOKEN: your-mapbox-access-token 7 | MAPBOX_MAP_ID: mapbox/outdoors-v11 8 | MAPBOX_ATTRIBUTION: © Mapbox © OpenStreetMap Improve this map 9 | # MAP_BASE_URL: http://tile.openstreetmap.org/{z}/{x}/{y}.png # Uncomment it if maps are gray 10 | SLACK_CHANNEL: optional-slack-channel 11 | SLACK_WEBHOOK: optional-slack-webhook 12 | GRAPH_HOPPER_KEY: your-graph-hopper-key 13 | # Optional override to use a custom service instead of the graphhopper.com hosted service. 14 | # GRAPH_HOPPER_URL: http://localhost:8989/ 15 | # Optional overrides to use custom service or different api key for certain bounding boxes. 16 | # (uncomment below to enable) 17 | # GRAPH_HOPPER_ALTERNATES: 18 | # - URL: http://localhost:8989/ 19 | # KEY: your-localhost-graph-hopper-key 20 | # BBOX: 21 | # - -170 22 | # - 6 23 | # - -46 24 | # - 83 25 | GOOGLE_ANALYTICS_TRACKING_ID: optional-ga-key 26 | # GRAPH_HOPPER_POINT_LIMIT: 10 # Defaults to 30 27 | DISABLE_AUTH: true 28 | -------------------------------------------------------------------------------- /configurations/default/settings.yml: -------------------------------------------------------------------------------- 1 | # Mastarm/deployment configuration properties. Note: application configuration 2 | # is now taken entirely from server application (there are still some 3 | # UI-specific env variables are in this repository's env.yml). 4 | cloudfront: DEPLOY-DISTRIBUTION 5 | entries: 6 | - 'lib/main.js:dist/index.js' 7 | - 'lib/index.css:dist/index.css' 8 | s3Bucket: bucket-for-deployment 9 | -------------------------------------------------------------------------------- /configurations/end-to-end/env.yml.tmp: -------------------------------------------------------------------------------- 1 | username: user@email.com 2 | password: password 3 | -------------------------------------------------------------------------------- /configurations/end-to-end/test-gtfs-to-fetch.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibi-group/datatools-ui/70adc13a79cdfe8c2c6eadafbe2a581a375d26ed/configurations/end-to-end/test-gtfs-to-fetch.zip -------------------------------------------------------------------------------- /configurations/end-to-end/test-gtfs-to-upload.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibi-group/datatools-ui/70adc13a79cdfe8c2c6eadafbe2a581a375d26ed/configurations/end-to-end/test-gtfs-to-upload.zip -------------------------------------------------------------------------------- /configurations/test/env.yml: -------------------------------------------------------------------------------- 1 | AUTH0_CLIENT_ID: mock-client-id 2 | AUTH0_CONNECTION_NAME: auth0-connection-name 3 | AUTH0_DOMAIN: test.domain.com 4 | GRAPH_HOPPER_KEY: test 5 | MAPBOX_ACCESS_TOKEN: test 6 | MAPBOX_MAP_ID: test 7 | MAPBOX_ATTRIBUTION: © Mapbox © OpenStreetMap Improve this map 8 | -------------------------------------------------------------------------------- /configurations/test/settings.yml: -------------------------------------------------------------------------------- 1 | # Unit tests must run without the server config, so this config file contains 2 | # configuration information that would otherwise be defined within the server.yml 3 | # file in datatools-server. 4 | application: 5 | active_project: project-id 6 | changelog_url: 'https://changelog.example.com' 7 | data: 8 | gtfs_s3_bucket: bucket-name 9 | use_s3_storage: false 10 | date_format: MMM Do YYYY 11 | docs_url: 'http://docs.example.com' 12 | logo: 'http://example.com/data_manager.png' 13 | notifications_enabled: false 14 | profileRefreshTime: -1 15 | support_email: support@example.com 16 | title: Data Manager 17 | entries: 18 | - 'lib/main.js:dist/index.js' 19 | - 'lib/index.css:dist/index.css' 20 | extensions: 21 | transitfeeds: 22 | enabled: false 23 | transitland: 24 | enabled: false 25 | modules: 26 | deployment: 27 | enabled: true 28 | editor: 29 | enabled: true 30 | enterprise: 31 | enabled: true 32 | user_admin: 33 | enabled: true 34 | validator: 35 | enabled: true 36 | -------------------------------------------------------------------------------- /docker/server/env.yml: -------------------------------------------------------------------------------- 1 | DISABLE_AUTH: TRUE 2 | GTFS_DATABASE_URL: jdbc:postgresql://postgres/dmtest 3 | MONGO_DB_NAME: data_manager 4 | MONGO_HOST: mongo 5 | AUTH0_CLIENT_ID: disable_auth -------------------------------------------------------------------------------- /docker/ui/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14 2 | WORKDIR /datatools-build 3 | 4 | ARG BUGSNAG_KEY 5 | 6 | RUN cd /datatools-build 7 | COPY package.json yarn.lock patches /datatools-build/ 8 | RUN yarn 9 | COPY . /datatools-build/ 10 | COPY configurations/default /datatools-config/ 11 | 12 | 13 | # Copy the tmp file to the env.yml if no env.yml is present 14 | RUN cp -R -u -p /datatools-config/env.yml.tmp /datatools-config/env.yml 15 | 16 | CMD yarn run mastarm build --env dev --serve --proxy http://datatools-server:4000/api # -------------------------------------------------------------------------------- /docs/dev/migration.md: -------------------------------------------------------------------------------- 1 | # Migration 2 | 3 | ## Migrating manager application data 4 | datatools-server offers a way to migrate application data (e.g., due to either breaking application schema changes or server changes). **Note:** this process requires temporarily exposing a `GET` request that exposes the entirety of the manager database. 5 | 6 | 1. Set the config setting `modules:dump:enabled` to `true`. 7 | 2. Restart the application. 8 | 3. Download copy of application data to local json file `curl localhost:4000/dump > db_backup.json`. 9 | 4. Change dump config setting back to `false`. 10 | 5. (optional) If looking to reload into the existing server, delete the manager mapdb (`.db` and `.dbp`) files in `application:data:mapdb` 11 | 6. Follow instructions in [`/scripts/load.py`](https://github.com/ibi-group/datatools-ui/blob/master/scripts/load.py) to upload the json data to the new server. 12 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Arcadis TRANSIT-data-tools 2 | 3 | The Arcadis TRANSIT-data-tools suite provides web-based tools for creating, managing, evaluating, and publishing transit data, specifically data stored in the General Transit Feed Specification (GTFS) format. 4 | 5 | ![feed-profile](https://datatools-builds.s3.amazonaws.com/docs/intro/feed-profile.png) 6 | 7 | To get started, select a topic from the table of contents on the left pane. 8 | -------------------------------------------------------------------------------- /docs/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Arial, Helvetica, sans-serif; 3 | } 4 | 5 | h1, h2, h3 { 6 | color: #015f97; 7 | font-family: Arial, Helvetica, sans-serif; 8 | } 9 | 10 | .img-center { 11 | margin-left: auto; 12 | margin-right: auto; 13 | width: 300px; 14 | } 15 | 16 | /* Make all images responsive */ 17 | img { 18 | display: block; 19 | margin: auto; 20 | } 21 | 22 | img[alt=screenshot] { 23 | width: 100%; 24 | } 25 | 26 | /* Center all iframes */ 27 | iframe { 28 | display: block; 29 | margin: 12px auto; 30 | } 31 | 32 | /* Ignore the first heading in the page in TOC list */ 33 | li.toctree-l3:first-child { 34 | display: none; 35 | } 36 | 37 | -------------------------------------------------------------------------------- /docs/user/introduction.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | ## Conceptual Overview 4 | 5 | The GTFS Data Manager enables exchange and coordination of data creation, updates, validation and deployment of GTFS data feeds for transit schedules. 6 | 7 | GTFS producers (transit operators, local governments, etc.) can share existing feeds or use the build function in GTFS Editor to create and maintain feeds. GTFS creators can use the built in validator to check for potential issues. 8 | 9 | ## Data Manager Concepts 10 | 11 | ### Projects 12 | 13 | Projects are collections of feed sources and deployments. 14 | 15 | ### Feed Sources 16 | 17 | Feed sources define the locations or upstream sources of GTFS feeds. These can be any combination of feeds that are: 18 | 19 | 1. **Manually Uploaded**: Manually collected/managed feeds provided directly by an external source. 20 | 2. **Fetched Automatically**: Public available feeds that can be fetched from a URL. 21 | 3. **Produced In House**: Internally managed/created feeds produced by GTFS Editor. 22 | 23 | ### Feed Versions 24 | 25 | Feed Versions store specific instances of a GTFS feed for a given feed source as they are published over time. Each Feed Version has an associated GTFS file that is stored within the Data Manager, which can be downloaded by users, where users can view detailed information about that version of the GTFS including validation results. 26 | 27 | ### Snapshots 28 | 29 | Internally managed GTFS data sets are pulled from the GTFS Editor using “snapshots” created in the editor interface. These snapshots are static versions of the data set, that serve as save points that can be exported or used as starting point for future edits. The Data Manager only imports versions of feeds where a snapshot has been created. This allows users to ensure the correct version of data is being imported and to retrieve and review data in the future. 30 | -------------------------------------------------------------------------------- /docs/user/otp-deployment.md: -------------------------------------------------------------------------------- 1 | # OTP Deployment Guide 2 | 3 | ## Overview 4 | 5 | This guide describes how to configure and deploy OTP servers using OTP TRANSIT-data-tools, and is for intermediate to advanced OTP TRANSIT-data-tools administrators. 6 | 7 | 8 | The following steps outline the process for performing an OTP deployment, covering the setup and linking of OTP servers to load balancers, S3 servers to CloudFront, and the integration of these various AWS resources within TRANSIT-data-tools. Administrators will also find instructions on how to configure optional subdomains (i.e., public URLs) for OTP servers. 9 | 10 | ## OTP Deployment Architecture 11 | 12 | The deployment architecture diagram below depicts how OTP servers are managed by TRANSIT-data-tools and can be used with elastic load balancers. The user interface is deployed on Amazon S3 servers and optionally mirrored by CloudFront, a high-bandwidth content delivery mechanism. TRANSIT-data-tools prepares and sends a data bundle and configuration properties to initialize OTP servers. The data bundle includes a set of GTFS feeds and OpenStreetMap data. DataTools makes the request to the osm-lib server and then creates a bundle of the resulting OSM and GTFS data. TRANSIT-data-tools does not manage UI deployments at this time. 13 | 14 | 15 | 16 | ## Resources for Performing an OTP Deployment 17 | 18 | 1. [Setting up OTP UI and backend servers on AWS](./setting-up-aws-servers.md) 19 | 2. [Adding a deployment server from TRANSIT-data-tools](./add-deployment-server.md) 20 | 3. [Deploying GTFS feeds to OTP](./deploying-feeds.md) 21 | -------------------------------------------------------------------------------- /flow-typed/npm/@conveyal/lonlat_v1.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 801d87d0bccd96d1e1a9c0fd6a351c79 2 | // flow-typed version: <>/@conveyal/lonlat_v^1.3.0/flow_v0.37.0 3 | 4 | declare module '@conveyal/lonlat' { 5 | declare type coordinatesInput = [number, number] 6 | declare type objectInput = { 7 | lat?: number, 8 | latitude?: number, 9 | lon?: number, 10 | lng?: number, 11 | longitude?: number 12 | } 13 | declare type pointInput = {x: number, y: number} 14 | declare type standardizedLonLat = { 15 | lat: number, 16 | lon: number 17 | } 18 | 19 | declare export default function normalize(mixed): standardizedLonLat 20 | 21 | declare export function isEqual(mixed, mixed, ?number): boolean 22 | declare export function print(mixed): string 23 | declare export function toCoordinates(mixed): [number, number] 24 | declare export function toLeaflet(mixed): {lat: number, lng: number} 25 | } 26 | -------------------------------------------------------------------------------- /flow-typed/npm/babel-polyfill_v6.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 28eccd914ac7bd65de204cb1d8d37cfe 2 | // flow-typed version: 7b122e75af/babel-polyfill_v6.x.x/flow_>=v0.30.x 3 | 4 | declare module 'babel-polyfill' {} 5 | -------------------------------------------------------------------------------- /flow-typed/npm/flow-bin_v0.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 6a5610678d4b01e13bbfbbc62bdaf583 2 | // flow-typed version: 3817bc6980/flow-bin_v0.x.x/flow_>=v0.25.x 3 | 4 | declare module "flow-bin" { 5 | declare module.exports: string; 6 | } 7 | -------------------------------------------------------------------------------- /flow-typed/npm/isomorphic-fetch_v2.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 33bbf70064fc58400833489a101165a7 2 | // flow-typed version: 45acb9a3f7/isomorphic-fetch_v2.x.x/flow_>=v0.25.x 3 | 4 | declare module "isomorphic-fetch" { 5 | declare module.exports: ( 6 | input: string | Request | URL, 7 | init?: RequestOptions 8 | ) => Promise; 9 | } 10 | -------------------------------------------------------------------------------- /flow-typed/npm/lodash.throttle_v4.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: b1bcad4f459d3e23e1460c8b17a4feea 2 | // flow-typed version: <>/lodash.throttle_v^4.1.1/flow_v0.37.0 3 | 4 | declare module 'lodash.throttle' { 5 | declare export default function throttle((...T) => void, number): (...T) => void 6 | } 7 | -------------------------------------------------------------------------------- /flow-typed/npm/lodash.tolower_v4.x.x.js: -------------------------------------------------------------------------------- 1 | declare module 'lodash.tolower' { 2 | declare export default (string) => string 3 | } 4 | -------------------------------------------------------------------------------- /flow-typed/npm/lodash.upperfirst_v4.x.x.js: -------------------------------------------------------------------------------- 1 | declare module 'lodash.upperfirst' { 2 | declare export default (string) => string 3 | } 4 | -------------------------------------------------------------------------------- /flow-typed/npm/object-path_v0.11.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 8e08c7b448e19eef73737b79fa31c8ae 2 | // flow-typed version: <>/object-path_v0.11.4/flow_v0.53.1 3 | 4 | declare module 'object-path' { 5 | declare module.exports: { 6 | ensureExists(obj: ?Object | Array, string | Array, ?any): ?any, 7 | get(obj: ?Object | Array, string | Array): ?any, 8 | set(obj: ?Object | Array, string, any): ?any 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /flow-typed/npm/polyline_v0.2.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: e8af478fdd21bcfff8ff15754086c5b2 2 | // flow-typed version: <>/polyline_v^0.2.0/flow_v0.53.1 3 | 4 | type Coordinate = [number, number] 5 | 6 | declare module 'polyline' { 7 | declare module.exports: { 8 | decode(string): Coordinate[], 9 | encode(Coordinate[]): string 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /flow-typed/npm/prop-types_v15.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: d9a983bb1ac458a256c31c139047bdbb 2 | // flow-typed version: 927687984d/prop-types_v15.x.x/flow_>=v0.41.x 3 | 4 | type $npm$propTypes$ReactPropsCheckType = ( 5 | props: any, 6 | propName: string, 7 | componentName: string, 8 | href?: string) => ?Error; 9 | 10 | declare module 'prop-types' { 11 | declare var array: React$PropType$Primitive>; 12 | declare var bool: React$PropType$Primitive; 13 | declare var func: React$PropType$Primitive; 14 | declare var number: React$PropType$Primitive; 15 | declare var object: React$PropType$Primitive; 16 | declare var string: React$PropType$Primitive; 17 | declare var symbol: React$PropType$Primitive; 18 | declare var any: React$PropType$Primitive; 19 | declare var arrayOf: React$PropType$ArrayOf; 20 | declare var element: React$PropType$Primitive; /* TODO */ 21 | declare var instanceOf: React$PropType$InstanceOf; 22 | declare var node: React$PropType$Primitive; /* TODO */ 23 | declare var objectOf: React$PropType$ObjectOf; 24 | declare var oneOf: React$PropType$OneOf; 25 | declare var oneOfType: React$PropType$OneOfType; 26 | declare var shape: React$PropType$Shape; 27 | 28 | declare function checkPropTypes( 29 | propTypes: $Subtype<{[_: $Keys]: $npm$propTypes$ReactPropsCheckType}>, 30 | values: V, 31 | location: string, 32 | componentName: string, 33 | getStack: ?(() => ?string) 34 | ) : void; 35 | } 36 | -------------------------------------------------------------------------------- /flow-typed/npm/qs_v6.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: c83e1b4bfe8580e9003b7e0a111276b9 2 | // flow-typed version: <>/qs_v^6.2.1/flow_v0.53.1 3 | 4 | declare module 'qs' { 5 | declare module.exports: { 6 | stringify(Object): string 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /flow-typed/npm/react-dnd-html5-backend_v2.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: b517cd8873674b6ac09e799503836168 2 | // flow-typed version: 30815cf324/react-dnd-html5-backend_v2.x.x/flow_>=v0.25.x 3 | 4 | declare type $npm$reactDnd$NativeTypes$FILE = "__NATIVE_FILE__"; 5 | declare type $npm$reactDnd$NativeTypes$URL = "__NATIVE_URL__"; 6 | declare type $npm$reactDnd$NativeTypes$TEXT = "__NATIVE_TEXT__"; 7 | declare type $npm$reactDnd$NativeTypes = 8 | | $npm$reactDnd$NativeTypes$FILE 9 | | $npm$reactDnd$NativeTypes$URL 10 | | $npm$reactDnd$NativeTypes$TEXT; 11 | 12 | declare module "react-dnd-html5-backend" { 13 | declare module.exports: { 14 | getEmptyImage(): Image, 15 | NativeTypes: { 16 | FILE: $npm$reactDnd$NativeTypes$FILE, 17 | URL: $npm$reactDnd$NativeTypes$URL, 18 | TEXT: $npm$reactDnd$NativeTypes$TEXT 19 | } 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /flow-typed/npm/turf-point_v2.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 3457aa4eecf0797ededf7acbdec325e0 2 | // flow-typed version: da30fe6876/turf-point_v2.x.x/flow_>=v0.25.x 3 | 4 | // @flow 5 | 6 | type $npm$Turf$Point$Point = { 7 | type: "Point", 8 | coordinates: [number, number], 9 | bbox?: Array, 10 | crs?: { type: string, properties: mixed } 11 | }; 12 | 13 | type $npm$Turf$Destination$FeaturePoint = { 14 | type: "Feature", 15 | geometry: $npm$Turf$Point$Point, 16 | properties: ?{ [key: string]: ?Properties }, 17 | bbox?: Array, 18 | crs?: { type: string, properties: mixed } 19 | }; 20 | 21 | declare module "turf-point" { 22 | declare module.exports: ( 23 | coordinates: [number, number], 24 | properties?: Properties 25 | ) => $npm$Turf$Destination$FeaturePoint; 26 | } 27 | -------------------------------------------------------------------------------- /flow-typed/npm/turf-polygon_v1.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: f745a75864c8e3f18e215f5102636b3d 2 | // flow-typed version: da30fe6876/turf-polygon_v1.x.x/flow_>=v0.25.x 3 | 4 | // @flow 5 | 6 | type $npm$turfPolygon$LineRing = Array<[number, number]>; 7 | 8 | type $npm$turfPolygon$Polygon = { 9 | type: "Polygon", 10 | coordinates: Array<$npm$turfPolygon$LineRing>, 11 | bbox?: Array, 12 | crs?: { type: string, properties: mixed } 13 | }; 14 | 15 | type $npm$turfPolygon$FeaturePolygon = { 16 | type: "Feature", 17 | geometry: $npm$turfPolygon$Polygon, 18 | properties: ?{ [key: string]: ?Properties }, 19 | bbox?: Array, 20 | crs?: { type: string, properties: mixed } 21 | }; 22 | 23 | declare module "turf-polygon" { 24 | declare module.exports: ( 25 | Array>, 26 | properties?: Properties 27 | ) => $npm$turfPolygon$FeaturePolygon; 28 | } 29 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Data Tools 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /lib/admin/components/permissions.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type {Permission} from '../../common/user/UserPermissions' 4 | import {getComponentMessages} from '../../common/util/config' 5 | 6 | const messages = getComponentMessages('Permissions') 7 | 8 | const permissions: Array = [ 9 | { 10 | type: 'manage-feed', 11 | name: messages('manage-feed'), 12 | feedSpecific: true 13 | }, 14 | { 15 | type: 'edit-gtfs', 16 | name: messages('edit-gtfs'), 17 | feedSpecific: true 18 | }, 19 | { 20 | type: 'approve-gtfs', 21 | name: messages('approve-gtfs'), 22 | feedSpecific: true 23 | }, 24 | { 25 | type: 'edit-alert', 26 | name: messages('edit-alert'), 27 | feedSpecific: true, 28 | module: 'alerts' 29 | }, 30 | { 31 | type: 'approve-alert', 32 | name: messages('approve-alert'), 33 | feedSpecific: true, 34 | module: 'alerts' 35 | } 36 | ] 37 | 38 | export default permissions 39 | -------------------------------------------------------------------------------- /lib/admin/reducers/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { combineReducers } from 'redux' 4 | 5 | import organizations from './organizations' 6 | import servers from './servers' 7 | import users from './users' 8 | 9 | export default combineReducers({ 10 | organizations, 11 | servers, 12 | users 13 | }) 14 | -------------------------------------------------------------------------------- /lib/admin/reducers/organizations.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import update from 'react-addons-update' 4 | 5 | import type {Action} from '../../types/actions' 6 | import type {OrganizationsState} from '../../types/reducers' 7 | 8 | export const defaultState = { 9 | isFetching: false, 10 | data: null, 11 | userQueryString: null 12 | } 13 | 14 | const organizations = ( 15 | state: OrganizationsState = defaultState, 16 | action: Action 17 | ): OrganizationsState => { 18 | switch (action.type) { 19 | case 'REQUESTING_ORGANIZATIONS': 20 | return update(state, {isFetching: { $set: true }}) 21 | case 'RECEIVE_ORGANIZATIONS': 22 | return update(state, { 23 | isFetching: { $set: false }, 24 | data: { $set: action.payload } 25 | }) 26 | case 'CREATED_ORGANIZATION': 27 | if (state.data) { 28 | return update(state, {data: { $push: [action.payload] }}) 29 | } 30 | return state 31 | default: 32 | return state 33 | } 34 | } 35 | 36 | export default organizations 37 | -------------------------------------------------------------------------------- /lib/admin/reducers/servers.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import update from 'react-addons-update' 4 | 5 | import type { Action } from '../../types/actions' 6 | import type { AdminServersState } from '../../types/reducers' 7 | 8 | export const defaultState = { 9 | isFetching: false, 10 | data: null 11 | } 12 | 13 | const servers = (state: AdminServersState = defaultState, action: Action): AdminServersState => { 14 | switch (action.type) { 15 | case 'REQUESTING_SERVERS': 16 | return update(state, { isFetching: { $set: true } }) 17 | case 'RECEIVE_SERVERS': 18 | return update(state, { 19 | isFetching: { $set: false }, 20 | data: { $set: action.payload } 21 | }) 22 | case 'RECEIVE_SERVER': 23 | const serverData = action.payload 24 | if (state.data) { 25 | const serverIdx = state.data.findIndex( 26 | server => server.id === serverData.id 27 | ) 28 | return update(state, { 29 | isFetching: { $set: false }, 30 | data: { [serverIdx]: { $set: action.payload } } 31 | }) 32 | } 33 | return state 34 | case 'CREATED_SERVER': 35 | if (state.data) { 36 | return update(state, { data: { $push: [action.payload] } }) 37 | } 38 | return state 39 | default: 40 | return state 41 | } 42 | } 43 | 44 | export default servers 45 | -------------------------------------------------------------------------------- /lib/admin/reducers/users.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import update from 'react-addons-update' 4 | 5 | import type {Action} from '../../types/actions' 6 | import type {AdminUsersState} from '../../types/reducers' 7 | 8 | export const defaultState = { 9 | data: null, 10 | isFetching: false, 11 | page: 0, 12 | perPage: 10, 13 | userCount: 0, 14 | userQueryString: null 15 | } 16 | 17 | const users = (state: AdminUsersState = defaultState, action: Action): AdminUsersState => { 18 | switch (action.type) { 19 | case 'REQUESTING_USERS': 20 | return update(state, {isFetching: { $set: true }}) 21 | case 'RECEIVE_USERS': 22 | const {totalUserCount, users} = action.payload 23 | return update(state, { 24 | isFetching: { $set: false }, 25 | data: { $set: users }, 26 | userCount: { $set: totalUserCount } 27 | }) 28 | case 'CREATED_USER': 29 | if (state.data) { 30 | return update(state, {data: { $push: [action.payload] }}) 31 | } 32 | return state 33 | case 'SET_USER_PAGE': 34 | return update(state, {page: { $set: action.payload }}) 35 | case 'SET_USER_PER_PAGE': 36 | return update(state, {perPage: { $set: action.payload }}) 37 | case 'SET_USER_QUERY_STRING': 38 | return update(state, {userQueryString: { $set: action.payload }}) 39 | default: 40 | return state 41 | } 42 | } 43 | 44 | export default users 45 | -------------------------------------------------------------------------------- /lib/alerts/actions/activeAlert.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import {createAction, type ActionType} from 'redux-actions' 4 | 5 | import type {AlertEntity} from '../../types' 6 | import type {dispatchFn, getStateFn} from '../../types/reducers' 7 | 8 | export const deleteActiveEntity = createAction( 9 | 'DELETE_ACTIVE_ALERT_AFFECTED_ENTITY', 10 | (payload: any) => payload 11 | ) 12 | const newEntity = createAction( 13 | 'ADD_ACTIVE_ALERT_AFFECTED_ENTITY', 14 | (payload: { 15 | agency: any, 16 | type: string, 17 | [string]: any 18 | }) => payload 19 | ) 20 | export const setActiveProperty = createAction( 21 | 'SET_ACTIVE_ALERT_PROPERTY', 22 | (payload: { [string]: any }) => payload 23 | ) 24 | export const setActivePublished = createAction( 25 | 'SET_ACTIVE_ALERT_PUBLISHED', 26 | (payload: boolean) => payload 27 | ) 28 | export const updateActiveEntity = createAction( 29 | 'UPDATE_ACTIVE_ALERT_ENTITY', 30 | (payload: { 31 | agency?: any, 32 | entity: AlertEntity, 33 | field: string, 34 | value: any 35 | }) => payload 36 | ) 37 | 38 | export type ActiveAlertActions = ActionType | 39 | ActionType | 40 | ActionType | 41 | ActionType | 42 | ActionType 43 | 44 | export function addActiveEntity ( 45 | field: string = 'AGENCY', 46 | value: any = null, 47 | agency: any = null 48 | ) { 49 | return function (dispatch: dispatchFn, getState: getStateFn) { 50 | return dispatch(newEntity({ 51 | type: field, 52 | agency, 53 | [field.toLowerCase()]: value 54 | })) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/alerts/actions/visibilityFilter.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import {createAction, type ActionType} from 'redux-actions' 4 | 5 | export const setAlertAgencyFilter = createAction( 6 | 'SET_ALERT_AGENCY_FILTER', 7 | (payload: string) => payload 8 | ) 9 | export const setAlertSort = createAction( 10 | 'SET_ALERT_SORT', 11 | (payload: { 12 | direction: string, 13 | type: string 14 | }) => payload 15 | ) 16 | export const setVisibilityFilter = createAction( 17 | 'SET_ALERT_VISIBILITY_FILTER', 18 | (payload: string) => payload 19 | ) 20 | export const setVisibilitySearchText = createAction( 21 | 'SET_ALERT_VISIBILITY_SEARCH_TEXT', 22 | (payload: string) => payload 23 | ) 24 | 25 | export type AlertVisibilityFilterActions = ActionType | 26 | ActionType | 27 | ActionType | 28 | ActionType 29 | -------------------------------------------------------------------------------- /lib/alerts/components/AgencySelector.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React, {Component} from 'react' 4 | import {FormControl} from 'react-bootstrap' 5 | 6 | import * as activeAlertActions from '../actions/activeAlert' 7 | import {getFeed, getFeedId} from '../../common/util/modules' 8 | 9 | import type {AlertEntity, Feed} from '../../types' 10 | 11 | type Props = { 12 | entity: AlertEntity, 13 | feeds: Array, 14 | updateActiveEntity: typeof activeAlertActions.updateActiveEntity 15 | } 16 | 17 | export default class AgencySelector extends Component { 18 | _onSelect = (evt: SyntheticInputEvent) => { 19 | const {entity, feeds, updateActiveEntity} = this.props 20 | const feed = getFeed(feeds, evt.target.value) 21 | updateActiveEntity({entity, field: 'AGENCY', value: feed}) 22 | } 23 | 24 | render () { 25 | const {feeds, entity} = this.props 26 | return ( 27 |
28 | 32 | {feeds.map((feed) => ( 33 | 38 | ))} 39 | 40 |
41 | ) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/alerts/components/CreateAlert.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import Icon from '@conveyal/woonerf/components/icon' 4 | import React, {Component} from 'react' 5 | import { Button } from 'react-bootstrap' 6 | 7 | import * as alertActions from '../actions/alerts' 8 | 9 | type Props = { 10 | createAlert: typeof alertActions.createAlert, 11 | disabled: boolean, 12 | fetched: boolean 13 | } 14 | 15 | export default class CreateAlert extends Component { 16 | render () { 17 | const { 18 | createAlert, 19 | disabled, 20 | fetched 21 | } = this.props 22 | const createDisabled = disabled != null 23 | ? disabled || !fetched 24 | : false 25 | return ( 26 | 41 | ) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/alerts/components/ModeSelector.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React, {Component} from 'react' 4 | import { FormControl } from 'react-bootstrap' 5 | 6 | import * as activeAlertActions from '../actions/activeAlert' 7 | import {modes} from '../util' 8 | 9 | import type {AlertEntity} from '../../types' 10 | 11 | type Props = { 12 | entity: AlertEntity, 13 | updateActiveEntity: typeof activeAlertActions.updateActiveEntity 14 | } 15 | 16 | export default class ModeSelector extends Component { 17 | _onChange = (evt: SyntheticInputEvent) => { 18 | const {entity, updateActiveEntity} = this.props 19 | updateActiveEntity({entity, field: 'MODE', value: this.getMode(evt.target.value)}) 20 | } 21 | 22 | getMode = (routeType: string) => modes.find((mode) => mode.gtfsType === +routeType) 23 | 24 | render () { 25 | const {entity} = this.props 26 | return ( 27 |
28 | 32 | {modes.map((mode, i) => ( 33 | 34 | ))} 35 | 36 |
37 | ) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/alerts/components/RouteSelector.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React, {Component} from 'react' 4 | 5 | import * as activeAlertActions from '../actions/activeAlert' 6 | import {getRouteNameAlerts} from '../../editor/util/gtfs' 7 | import GtfsSearch from '../../gtfs/components/gtfs-search' 8 | 9 | import type {GtfsOption} from '../../gtfs/components/gtfs-search' 10 | import type {AlertEntity, Feed, GtfsRoute, GtfsStop} from '../../types' 11 | 12 | type Props = { 13 | clearable?: boolean, 14 | entity: AlertEntity, 15 | feeds: Array, 16 | filterByStop?: GtfsStop, 17 | minimumInput?: number, 18 | route?: GtfsRoute, 19 | updateActiveEntity: typeof activeAlertActions.updateActiveEntity 20 | } 21 | 22 | export default class RouteSelector extends Component { 23 | _onChange = (value: GtfsOption) => { 24 | const {entity, filterByStop, updateActiveEntity} = this.props 25 | const field = 'ROUTE' 26 | if (value) { 27 | updateActiveEntity({entity, field, value: value.route, agency: value.agency}) 28 | } else if (value == null) { 29 | if (filterByStop) { 30 | updateActiveEntity({entity, field, value: null, agency: entity.agency}) 31 | } else { 32 | updateActiveEntity({entity, field, value: null, agency: null}) 33 | } 34 | } 35 | } 36 | 37 | render () { 38 | const {route, feeds, minimumInput, filterByStop, clearable, entity} = this.props 39 | const {agency: feed} = entity 40 | const agencyName = feed ? feed.name : 'Unknown agency' 41 | const routeName = route 42 | ? getRouteNameAlerts(route) || '[no name]' 43 | : '[route not found!]' 44 | const value = route 45 | ? { 46 | route, 47 | value: route.route_id, 48 | label: `${routeName} (${agencyName})`, 49 | agency: feed 50 | } 51 | : '' 52 | return ( 53 | 62 | ) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/alerts/components/StopSelector.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React, {Component} from 'react' 4 | 5 | import * as activeAlertActions from '../actions/activeAlert' 6 | import GtfsSearch from '../../gtfs/components/gtfs-search' 7 | 8 | import type {GtfsOption} from '../../gtfs/components/gtfs-search' 9 | import type {AlertEntity, Feed, GtfsRoute, GtfsStop} from '../../types' 10 | 11 | type Props = { 12 | clearable?: boolean, 13 | entity: AlertEntity, 14 | feeds: Array, 15 | filterByRoute?: GtfsRoute, 16 | minimumInput?: number, 17 | stop: ?GtfsStop, 18 | updateActiveEntity: typeof activeAlertActions.updateActiveEntity 19 | } 20 | 21 | export default class StopSelector extends Component { 22 | _onChange = (value: GtfsOption) => { 23 | const {entity, updateActiveEntity} = this.props 24 | const field = 'STOP' 25 | if (value) updateActiveEntity({entity, field, value: value.stop, agency: value.agency}) 26 | else updateActiveEntity({entity, field, value: null, agency: null}) 27 | } 28 | 29 | render () { 30 | const { 31 | stop, 32 | feeds, 33 | minimumInput, 34 | filterByRoute, 35 | clearable, 36 | entity 37 | } = this.props 38 | const {agency: feed} = entity 39 | const agencyName = feed ? feed.name : 'Unknown agency' 40 | return ( 41 | 58 | ) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/alerts/containers/ActiveAlertEditor.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { connect } from 'react-redux' 4 | 5 | import { 6 | createAlert, 7 | deleteAlert, 8 | onAlertEditorMount, 9 | saveAlert 10 | } from '../actions/alerts' 11 | import { 12 | setActiveProperty, 13 | setActivePublished, 14 | addActiveEntity, 15 | deleteActiveEntity, 16 | updateActiveEntity 17 | } from '../actions/activeAlert' 18 | import AlertEditor from '../components/AlertEditor' 19 | import { getFeedsForPermission } from '../../common/util/permissions' 20 | import {getActiveFeeds} from '../../gtfs/selectors' 21 | import {getActiveProject} from '../../manager/selectors' 22 | 23 | import type {AppState, RouterProps} from '../../types/reducers' 24 | 25 | export type Props = RouterProps 26 | 27 | const mapStateToProps = (state: AppState, ownProps: Props) => { 28 | return { 29 | activeFeeds: getActiveFeeds(state), 30 | alert: state.alerts.active, 31 | editableFeeds: getFeedsForPermission(getActiveProject(state), state.user, 'edit-alert'), 32 | permissionFilter: state.gtfs.filter.permissionFilter, 33 | project: getActiveProject(state), 34 | publishableFeeds: getFeedsForPermission(getActiveProject(state), state.user, 'approve-alert'), 35 | user: state.user 36 | } 37 | } 38 | 39 | const mapDispatchToProps = { 40 | addActiveEntity, 41 | createAlert, 42 | deleteActiveEntity, 43 | deleteAlert, 44 | onAlertEditorMount, 45 | saveAlert, 46 | setActiveProperty, 47 | setActivePublished, 48 | updateActiveEntity 49 | } 50 | 51 | const ActiveAlertEditor = connect( 52 | mapStateToProps, 53 | mapDispatchToProps 54 | )(AlertEditor) 55 | 56 | export default ActiveAlertEditor 57 | -------------------------------------------------------------------------------- /lib/alerts/containers/MainAlertsViewer.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { connect } from 'react-redux' 4 | 5 | import {createAlert, fetchRtdAlerts, onAlertsViewerMount} from '../actions/alerts' 6 | import {addActiveEntity} from '../actions/activeAlert' 7 | import AlertsViewer from '../components/AlertsViewer' 8 | import {getActiveAndLoadedFeeds} from '../../gtfs/selectors' 9 | import {getActiveProject} from '../../manager/selectors' 10 | 11 | import type {AppState, RouterProps} from '../../types/reducers' 12 | 13 | export type Props = RouterProps 14 | 15 | const mapStateToProps = (state: AppState, ownProps: Props) => { 16 | return { 17 | activeFeeds: getActiveAndLoadedFeeds(state), 18 | alerts: state.alerts.all, 19 | fetched: state.alerts.fetched, 20 | isFetching: state.alerts.isFetching, 21 | permissionFilter: state.gtfs.filter.permissionFilter, 22 | project: getActiveProject(state), 23 | user: state.user 24 | } 25 | } 26 | 27 | const mapDispatchToProps = { 28 | addActiveEntity, 29 | createAlert, 30 | fetchRtdAlerts, 31 | onAlertsViewerMount 32 | } 33 | 34 | const MainAlertsViewer = connect( 35 | mapStateToProps, 36 | mapDispatchToProps 37 | )(AlertsViewer) 38 | 39 | export default MainAlertsViewer 40 | -------------------------------------------------------------------------------- /lib/alerts/containers/VisibleAlertsList.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import {connect} from 'react-redux' 4 | 5 | import { 6 | editAlert, 7 | deleteAlert 8 | } from '../actions/alerts' 9 | import { 10 | setAlertAgencyFilter, 11 | setAlertSort, 12 | setVisibilityFilter, 13 | setVisibilitySearchText 14 | } from '../actions/visibilityFilter' 15 | import {getFeedsForPermission} from '../../common/util/permissions' 16 | import AlertsList from '../components/AlertsList' 17 | import {getActiveProject} from '../../manager/selectors' 18 | import {getVisibleAlerts} from '../selectors' 19 | 20 | import type {AppState} from '../../types/reducers' 21 | 22 | export type Props = {} 23 | 24 | const mapStateToProps = (state: AppState, ownProps: Props) => { 25 | const activeProject = getActiveProject(state) 26 | return { 27 | alerts: getVisibleAlerts(state), 28 | editableFeeds: getFeedsForPermission(activeProject, state.user, 'edit-alert'), 29 | feeds: activeProject && activeProject.feedSources ? activeProject.feedSources : [], 30 | fetched: state.alerts.fetched, 31 | filterCounts: state.alerts.counts, 32 | isFetching: state.alerts.isFetching, 33 | publishableFeeds: getFeedsForPermission(activeProject, state.user, 'approve-alert'), 34 | visibilityFilter: state.alerts.filter 35 | } 36 | } 37 | 38 | const mapDispatchToProps = { 39 | deleteAlert, 40 | editAlert, 41 | setAlertAgencyFilter, 42 | setAlertSort, 43 | setVisibilityFilter, 44 | setVisibilitySearchText 45 | } 46 | 47 | const VisibleAlertsList = connect(mapStateToProps, mapDispatchToProps)(AlertsList) 48 | 49 | export default VisibleAlertsList 50 | -------------------------------------------------------------------------------- /lib/alerts/reducers/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import active from './active' 4 | import alerts from './alerts' 5 | 6 | export default alerts.merge(active) 7 | -------------------------------------------------------------------------------- /lib/alerts/selectors/index.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect' 2 | 3 | import { getFeedId } from '../../common/util/modules' 4 | import { filterAlertsByCategory } from '../util' 5 | 6 | export const getVisibleAlerts = createSelector( 7 | [state => state.alerts.all, state => state.alerts.filter], 8 | (alerts, visibilityFilter) => { 9 | if (!alerts) return [] 10 | 11 | // filter alerts by the search text string 12 | let visibleAlerts = alerts.filter(alert => 13 | alert.title.toLowerCase().indexOf((visibilityFilter.searchText || '').toLowerCase()) !== -1) 14 | 15 | if (visibilityFilter.feedId && visibilityFilter.feedId !== 'ALL') { 16 | // console.log('filtering alerts by feedId' + visibilityFilter.feedId) 17 | visibleAlerts = visibleAlerts.filter(alert => alert.affectedEntities.findIndex(ent => getFeedId(ent.agency) === visibilityFilter.feedId) !== -1) 18 | } 19 | 20 | if (visibilityFilter.sort) { 21 | // console.log('sorting alerts by ' + visibilityFilter.sort.type + ' direction: ' + visibilityFilter.sort.direction) 22 | visibleAlerts = visibleAlerts.sort((a, b) => { 23 | var aValue = visibilityFilter.sort.type === 'title' ? a[visibilityFilter.sort.type].toUpperCase() : a[visibilityFilter.sort.type] 24 | var bValue = visibilityFilter.sort.type === 'title' ? b[visibilityFilter.sort.type].toUpperCase() : b[visibilityFilter.sort.type] 25 | if (aValue < bValue) return visibilityFilter.sort.direction === 'asc' ? -1 : 1 26 | if (aValue > bValue) return visibilityFilter.sort.direction === 'asc' ? 1 : -1 27 | return 0 28 | }) 29 | } else { 30 | // sort by id 31 | visibleAlerts.sort((a, b) => a.id - b.id) 32 | } 33 | return filterAlertsByCategory(visibleAlerts, visibilityFilter.filter) 34 | } 35 | ) 36 | -------------------------------------------------------------------------------- /lib/assets/application_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibi-group/datatools-ui/70adc13a79cdfe8c2c6eadafbe2a581a375d26ed/lib/assets/application_icon.png -------------------------------------------------------------------------------- /lib/assets/application_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibi-group/datatools-ui/70adc13a79cdfe8c2c6eadafbe2a581a375d26ed/lib/assets/application_logo.png -------------------------------------------------------------------------------- /lib/common/components/ClickOutside.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import * as React from 'react' 4 | 5 | type Props = { 6 | children: React.Node, 7 | onClickOutside: (MouseEvent | KeyboardEvent) => void 8 | } 9 | 10 | /** 11 | * Wrapper component that detects click or key press (ESC) and calls 12 | * onClickOutside function in response. 13 | */ 14 | export default class ClickOutside extends React.Component { 15 | container = null 16 | 17 | componentDidMount () { 18 | document.addEventListener('click', this.handle, true) 19 | document.addEventListener('keydown', this.handleKeyDown, true) 20 | } 21 | 22 | componentWillUnmount () { 23 | document.removeEventListener('click', this.handle, true) 24 | document.removeEventListener('keydown', this.handleKeyDown, true) 25 | } 26 | 27 | handle = (e: MouseEvent) => { 28 | const {onClickOutside} = this.props 29 | const el = this.container 30 | // $FlowFixMe 31 | if (!el.contains(e.target)) onClickOutside(e) 32 | } 33 | 34 | handleKeyDown = (e: KeyboardEvent) => { 35 | const {onClickOutside} = this.props 36 | // Handle ESC key press 37 | if (e.keyCode === 27) onClickOutside(e) 38 | } 39 | 40 | render () { 41 | const {children, onClickOutside, ...props} = this.props 42 | return
{ this.container = ref }}> 45 | {children} 46 |
47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/common/components/InfoModal.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from 'react' 4 | import { Modal, Button } from 'react-bootstrap' 5 | 6 | type Props = { 7 | body?: string, 8 | title?: string 9 | } 10 | 11 | type State = { 12 | body: string, 13 | showModal: boolean, 14 | title: string 15 | } 16 | 17 | export default class InfoModal extends React.Component { 18 | state = { 19 | body: '', 20 | showModal: false, 21 | title: '' 22 | } 23 | 24 | close () { 25 | this.setState({ 26 | showModal: false 27 | }) 28 | } 29 | 30 | open (props: Props) { 31 | this.setState({ 32 | showModal: true, 33 | title: props.title, 34 | body: props.body 35 | }) 36 | } 37 | 38 | ok = () => { 39 | this.close() 40 | } 41 | 42 | render () { 43 | const {Body, Footer, Header, Title} = Modal 44 | return ( 45 | 46 |
47 | {this.state.title} 48 |
49 | 50 | 51 |

{this.state.body}

52 | 53 | 54 |
55 | 59 |
60 |
61 | ) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/common/components/LanguageSelect.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React, {Component} from 'react' 4 | import { shallowEqual } from 'react-pure-render' 5 | import Select from 'react-select' 6 | import ISO6391 from 'iso-639-1' 7 | 8 | import {getComponentMessages} from '../util/config' 9 | 10 | type Props = { 11 | clearable?: boolean, 12 | minimumInput?: number, 13 | onChange?: string => void, 14 | placeholder?: string, 15 | tabIndex?: number, 16 | value: ?string 17 | } 18 | 19 | type State = { 20 | value: ?string 21 | } 22 | 23 | export default class LanguageSelect extends Component { 24 | messages = getComponentMessages('LanguageSelect') 25 | 26 | static defaultProps = { 27 | minimumInput: 1 28 | } 29 | 30 | componentWillMount () { 31 | this.setState({ 32 | value: this.props.value 33 | }) 34 | } 35 | 36 | componentWillReceiveProps (nextProps: Props) { 37 | if (!shallowEqual(nextProps.value, this.props.value)) { 38 | this.setState({value: nextProps.value}) 39 | } 40 | } 41 | 42 | _onChange = (value: string) => { 43 | const {onChange} = this.props 44 | this.setState({value}) 45 | onChange && onChange(value) 46 | } 47 | 48 | _getOptions = () => ISO6391.getAllCodes().map(code => ({value: code, label: ISO6391.getName(code)})) 49 | 50 | render () { 51 | const {clearable, tabIndex, placeholder, minimumInput} = this.props 52 | return ( 53 | 55 | ) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/common/components/Title.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import {Component} from 'react' 4 | 5 | type Props = { 6 | children: string 7 | } 8 | 9 | export default class Title extends Component { 10 | oldTitle = '' 11 | 12 | componentWillMount () { 13 | this.oldTitle = document.title 14 | document.title = this.props.children 15 | } 16 | 17 | componentWillUnmount () { 18 | document.title = this.oldTitle 19 | } 20 | 21 | componentWillReceiveProps (nextProps: Props) { 22 | if (nextProps.children !== this.props.children) { 23 | document.title = nextProps.children 24 | } 25 | } 26 | 27 | shouldComponentUpdate (nextProps: Props) { 28 | // Never update the component when the title changes. 29 | return false 30 | } 31 | 32 | render () { 33 | return null 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/common/components/UserButtons.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React, { Component } from 'react' 4 | import { Button, ButtonToolbar } from 'react-bootstrap' 5 | import { LinkContainer } from 'react-router-bootstrap' 6 | import Icon from '@conveyal/woonerf/components/icon' 7 | 8 | import { AUTH0_DISABLED } from '../constants' 9 | import { getComponentMessages } from '../../common/util/config' 10 | import type { ManagerUserState } from '../../types/reducers' 11 | 12 | type Props = { 13 | logout: () => any, 14 | user: ManagerUserState 15 | } 16 | 17 | /** 18 | * A common component containing buttons for standard user actions: "My 19 | * Account", "Site Admin", and "Logout" 20 | */ 21 | export default class UserButtons extends Component { 22 | messages = getComponentMessages('UserButtons') 23 | 24 | render () { 25 | const { logout, user } = this.props 26 | const buttonStyle = { margin: 2 } 27 | const isSiteAdmin = user.permissions && user.permissions.isApplicationAdmin() && 28 | user.permissions.canAdministerAnOrganization() 29 | return ( 30 | 31 | 32 | 35 | 36 | {isSiteAdmin && ( 37 | 38 | 44 | 45 | )} 46 | {/* "Log out" Button (unless auth is disabled) */} 47 | {!AUTH0_DISABLED && ( 48 | 55 | )} 56 | 57 | ) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lib/common/containers/ActiveSidebar.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { withAuth0 } from '@auth0/auth0-react' 4 | import { connect } from 'react-redux' 5 | 6 | import Sidebar from '../components/Sidebar' 7 | import * as userActions from '../../manager/actions/user' 8 | import * as statusActions from '../../manager/actions/status' 9 | import type {AppState} from '../../types/reducers' 10 | 11 | export type Props = {} 12 | 13 | const mapStateToProps = (state: AppState, ownProps: Props) => { 14 | return { 15 | appInfo: state.status.appInfo, 16 | expanded: state.ui.sidebarExpanded, 17 | hideTutorial: state.ui.hideTutorial, 18 | jobMonitor: state.status.jobMonitor, 19 | user: state.user 20 | } 21 | } 22 | 23 | const mapDispatchToProps = { 24 | logout: userActions.logout, 25 | removeRetiredJob: statusActions.removeRetiredJob, 26 | setJobMonitorVisible: statusActions.setJobMonitorVisible, 27 | startJobMonitor: statusActions.startJobMonitor 28 | } 29 | 30 | export default withAuth0(connect(mapStateToProps, mapDispatchToProps)(Sidebar)) 31 | -------------------------------------------------------------------------------- /lib/common/containers/ActiveSidebarNavItem.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { connect } from 'react-redux' 4 | 5 | import SidebarNavItem from '../components/SidebarNavItem' 6 | 7 | import type {AppState} from '../../types/reducers' 8 | 9 | export type Props = { 10 | 'data-test-id'?: string, 11 | active?: boolean, 12 | icon: string, 13 | label: string, 14 | link?: string 15 | } 16 | 17 | const mapStateToProps = (state: AppState, ownProps: Props) => { 18 | return { 19 | expanded: state.ui.sidebarExpanded 20 | } 21 | } 22 | 23 | const mapDispatchToProps = {} 24 | // $FlowFixMe https://github.com/flow-typed/flow-typed/issues/2628 25 | const ActiveSidebarNavItem = connect( 26 | mapStateToProps, 27 | mapDispatchToProps 28 | )(SidebarNavItem) 29 | 30 | export default ActiveSidebarNavItem 31 | -------------------------------------------------------------------------------- /lib/common/containers/AppInfoRetriever.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | // $FlowFixMe useEffect not recognized by flow. 3 | import { useEffect } from 'react' 4 | import { connect } from 'react-redux' 5 | 6 | import * as statusActions from '../../manager/actions/status' 7 | 8 | /** 9 | * Retrieves the app info and updates the redux state accordingly. 10 | */ 11 | const AppInfoRetriever = ({ fetchAppInfo }) => { 12 | // Fetch app info only once. 13 | useEffect(fetchAppInfo, []) 14 | 15 | // Component renders nothing. 16 | return null 17 | } 18 | 19 | const mapDispatchToProps = { 20 | fetchAppInfo: statusActions.fetchAppInfo 21 | } 22 | 23 | export default connect(null, mapDispatchToProps)(AppInfoRetriever) 24 | -------------------------------------------------------------------------------- /lib/common/containers/CurrentStatusMessage.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { connect } from 'react-redux' 4 | 5 | import StatusMessage from '../components/StatusMessage' 6 | 7 | import type {AppState} from '../../types/reducers' 8 | 9 | export type Props = {} 10 | 11 | const mapStateToProps = (state: AppState, ownProps: Props) => { 12 | return { 13 | message: state.status.message, 14 | sidebarExpanded: state.ui.sidebarExpanded 15 | } 16 | } 17 | 18 | var CurrentStatusMessage = connect( 19 | mapStateToProps 20 | )(StatusMessage) 21 | 22 | export default CurrentStatusMessage 23 | -------------------------------------------------------------------------------- /lib/common/containers/CurrentStatusModal.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { Auth0ContextInterface, withAuth0 } from '@auth0/auth0-react' 4 | import {connect} from 'react-redux' 5 | 6 | import StatusModal from '../components/StatusModal' 7 | import {clearStatusModal} from '../../manager/actions/status' 8 | import {removeEditorLock} from '../../editor/actions/editor' 9 | import type {AppState} from '../../types/reducers' 10 | 11 | export type Props = { 12 | auth0: Auth0ContextInterface 13 | } 14 | 15 | const mapStateToProps = (state: AppState) => { 16 | return { 17 | ...state.status.modal 18 | } 19 | } 20 | const mapDispatchToProps = { 21 | clearStatusModal, 22 | removeEditorLock 23 | } 24 | 25 | export default withAuth0(connect( 26 | mapStateToProps, 27 | mapDispatchToProps 28 | )(StatusModal)) 29 | -------------------------------------------------------------------------------- /lib/common/containers/LocalUserRetriever.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | // $FlowFixMe useEffect not recognized by flow. 3 | import { useEffect } from 'react' 4 | import { connect } from 'react-redux' 5 | 6 | import * as userActions from '../../manager/actions/user' 7 | import { AUTH0_CLIENT_ID } from '../constants' 8 | 9 | type Props = { 10 | receiveTokenAndProfile: typeof userActions.receiveTokenAndProfile 11 | } 12 | 13 | const profile = { 14 | app_metadata: { 15 | 'datatools': [ 16 | { 17 | 'permissions': [ 18 | { 19 | 'type': 'administer-application' 20 | } 21 | ], 22 | 'projects': [], 23 | 'client_id': AUTH0_CLIENT_ID, 24 | 'subscriptions': [] 25 | } 26 | ], 27 | 'roles': [ 28 | 'user' 29 | ] 30 | }, 31 | // FIXME: pick a better email address for both backend and frontend. 32 | email: 'mock@example.com', 33 | name: 'localuser', 34 | nickname: 'Local User', 35 | picture: 'https://d2tyb7byn1fef9.cloudfront.net/ibi_group_black-512x512.png', 36 | sub: 'localuser', 37 | user_id: 'localuser', 38 | user_metadata: {} 39 | } 40 | 41 | const token = 'local-user-token' 42 | 43 | /** 44 | * This component provides a user profile for configs without authentication. 45 | */ 46 | const LocalUserRetriever = ({ receiveTokenAndProfile }: Props) => { 47 | // Update the user info in the redux state on initialization. 48 | useEffect(() => { 49 | receiveTokenAndProfile({ profile, token }) 50 | }, []) 51 | 52 | // Component renders nothing. 53 | return null 54 | } 55 | 56 | const mapDispatchToProps = { 57 | receiveTokenAndProfile: userActions.receiveTokenAndProfile 58 | } 59 | 60 | export default connect(null, mapDispatchToProps)(LocalUserRetriever) 61 | -------------------------------------------------------------------------------- /lib/common/containers/PageContent.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import * as React from 'react' 4 | import {connect} from 'react-redux' 5 | 6 | import type {AppState} from '../../types/reducers' 7 | 8 | type ContainerProps = { 9 | children: React.Node 10 | } 11 | 12 | type Props = ContainerProps & { 13 | sidebarExpanded: boolean 14 | } 15 | 16 | class Content extends React.Component { 17 | render () { 18 | return ( 19 |
28 | {this.props.children} 29 |
30 | ) 31 | } 32 | } 33 | 34 | const mapStateToProps = (state: AppState, ownProps: ContainerProps) => { 35 | return { 36 | sidebarExpanded: state.ui.sidebarExpanded 37 | } 38 | } 39 | 40 | const mapDispatchToProps = {} 41 | 42 | var PageContent = connect( 43 | mapStateToProps, 44 | mapDispatchToProps 45 | )(Content) 46 | 47 | export default PageContent 48 | -------------------------------------------------------------------------------- /lib/common/containers/StarButton.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import Icon from '@conveyal/woonerf/components/icon' 4 | import React, {Component} from 'react' 5 | import {Button} from 'react-bootstrap' 6 | import {connect} from 'react-redux' 7 | 8 | import {getComponentMessages} from '../util/config' 9 | // $FlowFixMe FIXME action no longer present in user actions 10 | import {updateStar} from '../../manager/actions/user' 11 | 12 | import type {dispatchFn, ManagerUserState} from '../../types/reducers' 13 | 14 | type Props = { 15 | dispatch: dispatchFn, 16 | isStarred: boolean, 17 | target: string, 18 | user: ManagerUserState 19 | } 20 | 21 | class StarButton extends Component { 22 | messages = getComponentMessages('StarButton') 23 | 24 | _onClick = () => { 25 | const {dispatch, isStarred, user, target} = this.props 26 | dispatch(updateStar(user.profile, target, !isStarred)) 27 | } 28 | 29 | render () { 30 | const {isStarred} = this.props 31 | 32 | return ( 33 | 40 | ) 41 | } 42 | } 43 | 44 | export default connect()(StarButton) 45 | -------------------------------------------------------------------------------- /lib/common/user/UserSubscriptions.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { getSettingsForThisClient } from '../util/user' 4 | import type { DatatoolsSettings } from '../../types' 5 | 6 | export type Subscription = { 7 | target: Array, 8 | type: string 9 | } 10 | 11 | export default class UserSubscriptions { 12 | subscriptionLookup: {[string]: Subscription} = {} 13 | 14 | constructor (datatoolsApps: Array) { 15 | // If missing datatoolsApp, construct an empty subscriptions object. 16 | if (!datatoolsApps) return 17 | else if (!Array.isArray(datatoolsApps)) { 18 | console.warn('User app_metadata is misconfigured.', datatoolsApps) 19 | return 20 | } 21 | const datatoolsJson = getSettingsForThisClient(datatoolsApps) 22 | if (datatoolsJson && datatoolsJson.subscriptions) { 23 | for (const subscription of datatoolsJson.subscriptions) { 24 | this.subscriptionLookup[subscription.type] = subscription 25 | } 26 | } 27 | } 28 | hasSubscription (subscriptionType: string) { 29 | return this.subscriptionLookup[subscriptionType] !== null 30 | } 31 | 32 | getSubscription (subscriptionType: string) { 33 | return this.subscriptionLookup[subscriptionType] 34 | } 35 | 36 | hasProjectSubscription (projectId: string, subscriptionType: string) { 37 | if (!this.hasSubscription(subscriptionType)) return null 38 | const subscription = this.getSubscription(subscriptionType) 39 | return subscription ? subscription.target.indexOf(projectId) !== -1 : false 40 | } 41 | 42 | hasFeedSubscription (projectId: string, feedId: string, subscriptionType: string) { 43 | if (!this.hasSubscription(subscriptionType)) return null 44 | else if (this.hasProjectSubscription(projectId, subscriptionType)) return true 45 | const subscription = this.getSubscription(subscriptionType) 46 | return subscription ? subscription.target.indexOf(feedId) !== -1 : false 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/common/util/__tests__/config.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import {getComponentMessages} from '../config' 4 | 5 | describe('lib > common > util > config', () => { 6 | describe('> getComponentMessages', () => { 7 | let oldConfig 8 | 9 | afterEach(() => { 10 | window.DT_CONFIG = oldConfig 11 | }) 12 | 13 | beforeEach(() => { 14 | oldConfig = window.DT_CONFIG 15 | window.DT_CONFIG = { 16 | messages: { 17 | active: { 18 | components: { 19 | Breadcrumbs: { 20 | deployments: 'Deployments', 21 | projects: 'Projects', 22 | root: 'Explore' 23 | } 24 | } 25 | } 26 | } 27 | } 28 | }) 29 | 30 | it('should return message properly', () => { 31 | expect(getComponentMessages('Breadcrumbs')('root')).toEqual('Explore') 32 | }) 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /lib/common/util/__tests__/gtfs.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import {secondsAfterMidnightToHHMM} from '../gtfs' 4 | 5 | describe('lib > common > util > gtfs', () => { 6 | describe('> secondsAfterMidnightToHHMM', () => { 7 | describe('valid times', () => { 8 | it('should parse value 0', () => { 9 | expect(secondsAfterMidnightToHHMM(0)).toEqual('00:00:00') 10 | }) 11 | 12 | it('should parse a value in the day', () => { 13 | expect(secondsAfterMidnightToHHMM(12345)).toEqual('03:25:45') 14 | }) 15 | 16 | it('should parse a value after midnight', () => { 17 | expect(secondsAfterMidnightToHHMM(99999)).toEqual('27:46:39') 18 | }) 19 | 20 | it('should parse a value 2 days in the future', () => { 21 | expect(secondsAfterMidnightToHHMM(222222)).toEqual('61:43:42') 22 | }) 23 | 24 | it('should parse a value below 0', () => { 25 | expect(secondsAfterMidnightToHHMM(-1)).toEqual('23:59:59 (previous day)') 26 | }) 27 | }) 28 | 29 | describe('invalid times', () => { 30 | it('should not parse null', () => { 31 | expect(secondsAfterMidnightToHHMM(null)).toEqual('') 32 | }) 33 | }) 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /lib/common/util/__tests__/map.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import L from 'leaflet' 4 | 5 | import { coordIsOutOfBounds } from '../../../editor/util/map' 6 | 7 | describe('lib > common > util > map', () => { 8 | describe('> coordIsOutOfBounds', () => { 9 | const APPROX_NORTH_AMERICA_BOUNDS = L.latLngBounds([6.315299, -170.15625], [ 83.440326, -46.40625 ]) 10 | const NULL_BOUNDS = L.latLngBounds([0, 0], [0, 0]) 11 | const SOMEWHERE_IN_CANADA = {lat: 60.064840, lng: -99.492188} 12 | const NULL_ISLAND = {lat: 0, lng: 0} 13 | 14 | it('should correctly determine if a coordinate is outside set bounds', () => { 15 | expect(coordIsOutOfBounds(SOMEWHERE_IN_CANADA, APPROX_NORTH_AMERICA_BOUNDS)).toEqual(false) 16 | expect(coordIsOutOfBounds(NULL_ISLAND, APPROX_NORTH_AMERICA_BOUNDS)).toEqual(true) 17 | }) 18 | it('should correctly determine handle strange bounds', () => { 19 | expect(coordIsOutOfBounds(SOMEWHERE_IN_CANADA, NULL_BOUNDS)).toEqual(true) 20 | expect(coordIsOutOfBounds(NULL_ISLAND, NULL_BOUNDS)).toEqual(true) 21 | }) 22 | it('should correctly handle null/undefined arguments', () => { 23 | // $FlowFixMe this is a test 24 | expect(coordIsOutOfBounds(undefined, APPROX_NORTH_AMERICA_BOUNDS)).toEqual(true) 25 | // $FlowFixMe this is a test 26 | expect(coordIsOutOfBounds(SOMEWHERE_IN_CANADA, undefined)).toEqual(true) 27 | // $FlowFixMe this is a test 28 | expect(coordIsOutOfBounds(undefined, undefined)).toEqual(true) 29 | // $FlowFixMe this is a test 30 | expect(coordIsOutOfBounds()).toEqual(true) 31 | }) 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /lib/common/util/__tests__/permissions.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import {deploymentsEnabledAndAccessAllowedForProject} from '../permissions' 4 | 5 | import mockData from '../../../../__tests__/test-utils/mock-data' 6 | 7 | const {mockAdminUser, mockProjectWithDeployment} = mockData.manager 8 | 9 | describe('lib > common > util > permissions', () => { 10 | describe('> deploymentsEnabledAndAccessAllowedForProject', () => { 11 | const testCases = [ 12 | { 13 | description: 'a null project and any user', 14 | expectedResult: false, 15 | project: null, 16 | user: mockAdminUser 17 | }, { 18 | description: 'a project with deployments and an admin user', 19 | expectedResult: true, 20 | project: mockProjectWithDeployment, 21 | user: mockAdminUser 22 | } 23 | ] 24 | 25 | testCases.forEach(testCase => { 26 | it(`should return ${ 27 | String(testCase.expectedResult) 28 | } when given ${testCase.description}`, () => { 29 | expect(deploymentsEnabledAndAccessAllowedForProject( 30 | testCase.project, 31 | testCase.user 32 | )).toEqual(testCase.expectedResult) 33 | }) 34 | }) 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /lib/common/util/analytics.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import ReactGA from 'react-ga' 4 | 5 | // Check if Google Analytics is enabled for the application. 6 | const hasAnalytics: boolean = 7 | process.env.NODE_ENV !== 'dev' && 8 | process.env.NODE_ENV !== 'test' && 9 | !!process.env.GOOGLE_ANALYTICS_TRACKING_ID 10 | if (!hasAnalytics) console.warn('Google Analytics not enabled.') 11 | else ReactGA.initialize(process.env.GOOGLE_ANALYTICS_TRACKING_ID) 12 | 13 | /** 14 | * Log page views in Google Analytics (if enabled). 15 | */ 16 | export function logPageView (): void { 17 | if (hasAnalytics) { 18 | const page = `${window.location.pathname}${window.location.search}` 19 | ReactGA.set({page}) 20 | ReactGA.pageview(page) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/common/util/file-download.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import {saveAs} from 'file-saver' 3 | 4 | /** 5 | * This downloads a file using file-saver. Previously a custom method was used 6 | * (essentially the link.click simulation found here 7 | * https://stackoverflow.com/a/14966131/915811). However, that method no longer 8 | * works with the latest version of Chrome. 9 | */ 10 | export default function fileDownload (data: any, filename: string, type: string): void { 11 | const blob: Blob = new window.Blob([data], {type}) 12 | saveAs(blob, filename) 13 | } 14 | -------------------------------------------------------------------------------- /lib/common/util/geo.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type {Bounds, Feed, FeedWithValidation} from '../../types' 4 | 5 | export function getFeedsBounds (feeds: Array): ?Bounds { 6 | const feedsWithBounds: Array = ((feeds.filter( 7 | (feed: Feed) => feed.latestValidation && feed.latestValidation.bounds 8 | ): any): Array) 9 | if (feedsWithBounds.length === 1) { 10 | return feedsWithBounds[0].latestValidation.bounds 11 | } else if (feedsWithBounds.length === 0) { 12 | return null 13 | } else { 14 | const bounds: Bounds = feedsWithBounds[0].latestValidation.bounds 15 | feedsWithBounds.forEach((feed: FeedWithValidation) => { 16 | const curFeedBounds: Bounds = feed.latestValidation.bounds 17 | if (curFeedBounds.east > bounds.east) { 18 | bounds.east = curFeedBounds.east 19 | } 20 | if (curFeedBounds.north > bounds.north) { 21 | bounds.north = curFeedBounds.north 22 | } 23 | if (curFeedBounds.south < bounds.south) { 24 | bounds.south = curFeedBounds.south 25 | } 26 | if (curFeedBounds.west < bounds.west) { 27 | bounds.west = curFeedBounds.west 28 | } 29 | }) 30 | return bounds 31 | } 32 | } 33 | 34 | export function convertToArrayBounds (bounds: ?Bounds) { 35 | if (!bounds) throw new Error('Must provide valid bounds ({north, south, east, west})') 36 | else return [[bounds.north, bounds.east], [bounds.south, bounds.west]] 37 | } 38 | -------------------------------------------------------------------------------- /lib/common/util/gtfs.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import moment from 'moment' 4 | import {decode as decodePolyline} from 'polyline' 5 | 6 | type GraphQLShape = {polyline: string, shape_id: string} 7 | 8 | /** 9 | * @param {number} seconds Seconds after midnight 10 | * @return {string} A blank string if not a valid value, 11 | * or a string in the format HH:mm:ss where HH can be greater than 23 12 | */ 13 | export function secondsAfterMidnightToHHMM (seconds: ?(number | string)): string { 14 | if (typeof seconds === 'number') { 15 | const formattedValue = moment() 16 | .startOf('day') 17 | .seconds(seconds) 18 | .format('HH:mm:ss') 19 | if (seconds >= 86400) { 20 | // Replace hours part if seconds are greater than 24 hours (by default 21 | // moment.js does not handle times greater than 24h). 22 | const parts = formattedValue.split(':') 23 | parts[0] = '' + (parseInt(parts[0], 10) + 24 * Math.floor(seconds / 86400)) 24 | return parts.join(':') 25 | } else if (seconds < 0) { 26 | // probably an extreme edge case, but it's technically possible 27 | return `${formattedValue} (previous day)` 28 | } else { 29 | return formattedValue 30 | } 31 | } 32 | // If handling time format and value is not a number, return empty string. 33 | return '' 34 | } 35 | 36 | /** 37 | * Shorthand helper function to convert seconds value to human-readable text. 38 | */ 39 | export function humanizeSeconds (seconds: number): string { 40 | return moment.duration(seconds, 'seconds').humanize() 41 | } 42 | 43 | /** 44 | * Array map function to decode a GraphQL encoded shape polyline. 45 | */ 46 | export function decodeShapePolylines (shape: GraphQLShape) { 47 | return { 48 | id: shape.shape_id, 49 | // Decode polyline and coords divide by ten (gtfs-lib 50 | // simplification level requires this). 51 | latLngs: decodePolyline(shape.polyline) 52 | .map(coords => ([coords[0] / 10, coords[1] / 10])) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/common/util/json.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { analyze } from 'jju' 4 | 5 | /** 6 | * Check if a string is valid JSON. 7 | */ 8 | export function isValidJSON (str: string): boolean { 9 | try { 10 | JSON.parse(str) 11 | } catch (e) { 12 | return false 13 | } 14 | return true 15 | } 16 | 17 | /** 18 | * Check if a string is valid JSONC that OTP should be able to 19 | * parse. OTP allows comments and unquoted keys, but not other 20 | * fancy stuff. See OTP json parser here: 21 | * https://github.com/opentripplanner/OpenTripPlanner/blob/27f4ed0a86157bdd4c4bc3004fec25687768d373/src/main/java/org/opentripplanner/standalone/OTPMain.java#L190-L194 22 | */ 23 | export function isValidJSONC (str: string): boolean { 24 | try { 25 | const result = analyze(str) 26 | if ( 27 | // if JSON has quotes with single quotes, it is invalid 28 | result.quote_types.indexOf("'") > -1 || 29 | // if JSON has a multi-line quote, it is invalid 30 | result.has_multi_line_quote || 31 | // if JSON has trailing commas, it is invalid 32 | result.has_trailing_comma 33 | ) { 34 | return false 35 | } 36 | return true 37 | } catch (e) { 38 | return false 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/common/util/map-keys.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import forEach from 'lodash/forEach' 3 | import camelCase from 'lodash/camelCase' 4 | import isPlainObject from 'lodash/isPlainObject' 5 | import snakeCase from 'lodash/snakeCase' 6 | 7 | /** 8 | * Converts the keys for an object (or array of objects) using string mapping 9 | * function passed in. Operates on object recursively. 10 | */ 11 | function mapObjectKeys (object: Object, keyMapper: string => string): Object { 12 | const convertedObject = {} 13 | const convertedArray = [] 14 | forEach( 15 | object, 16 | (value: Object, key: string) => { 17 | if (isPlainObject(value) || Array.isArray(value)) { 18 | // If plain object or an array, recursively update keys of any values 19 | // that are also objects. 20 | value = mapObjectKeys(value, keyMapper) 21 | } 22 | if (Array.isArray(object)) convertedArray.push(value) 23 | else convertedObject[keyMapper(key)] = value 24 | } 25 | ) 26 | // $FlowFixMe 27 | if (Array.isArray(object)) return convertedArray 28 | else return convertedObject 29 | } 30 | 31 | /** 32 | * Converts the keys for an object or array of objects to camelCase. The function 33 | * always recursively converts keys. 34 | */ 35 | export function camelCaseKeys (object: Object): Object { 36 | return mapObjectKeys(object, camelCase) 37 | } 38 | 39 | /** 40 | * Converts the keys for an object or array of objects to snake_case. The function 41 | * always recursively converts keys. 42 | */ 43 | export function snakeCaseKeys (object: Object): Object { 44 | return mapObjectKeys(object, snakeCase) 45 | } 46 | -------------------------------------------------------------------------------- /lib/common/util/maps.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { Browser } from 'leaflet' 4 | 5 | import type {TileLayerProps, MapLayer} from '../../types' 6 | 7 | const DEFAULT_MAP_ID = 'mapbox/outdoors-v11' 8 | 9 | /** 10 | * Default map layers for the GTFS editor. (Note: this is defined in the common 11 | * util file in order to keep all refs to mapbox IDs in a single file.) 12 | */ 13 | export const EDITOR_MAP_LAYERS: Array = [ 14 | { 15 | name: 'Streets', 16 | id: DEFAULT_MAP_ID 17 | }, 18 | { 19 | name: 'Light', 20 | id: 'mapbox/light-v10' 21 | }, 22 | { 23 | name: 'Dark', 24 | id: 'mapbox/dark-v10' 25 | }, 26 | { 27 | name: 'Satellite', 28 | id: 'mapbox/satellite-streets-v11' 29 | } 30 | ] 31 | 32 | /** 33 | * Get the default Mapbox tile URL used for use in a leaflet map. Optionally 34 | * takes a map ID (e.g., mapbox/outdoors-v11). 35 | */ 36 | // eslint-disable-next-line complexity 37 | export function defaultTileLayerProps (mapId: ?string): TileLayerProps { 38 | // If no mapId is provided, default to id defined in env.yml or, ultimately, 39 | // fall back on default value. 40 | const id = mapId || process.env.MAPBOX_MAP_ID || DEFAULT_MAP_ID 41 | const attribution = process.env.MAPBOX_ATTRIBUTION || `© Mapbox © OpenStreetMap Improve this map` 42 | const MAPBOX_ACCESS_TOKEN = process.env.MAPBOX_ACCESS_TOKEN 43 | const MAP_BASE_URL = process.env.MAP_BASE_URL 44 | if (!MAPBOX_ACCESS_TOKEN && !MAP_BASE_URL) { 45 | throw new Error('One of Mapbox token or base url must be defined') 46 | } 47 | 48 | const retina = window.retina || window.devicePixelRatio > 1 || Browser.retina 49 | const url = process.env.MAP_BASE_URL || `https://api.mapbox.com/styles/v1/${id}/tiles/{z}/{x}/{y}${retina ? '@2x' : ''}?access_token=${MAPBOX_ACCESS_TOKEN || ''}` 50 | const retinaProps = retina 51 | ? {tileSize: 512, zoomOffset: -1} 52 | : {} 53 | return { 54 | attribution, 55 | ...retinaProps, 56 | url 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/common/util/modules.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import objectPath from 'object-path' 4 | 5 | import {getConfigProperty} from './config' 6 | 7 | import type {Feed} from '../../types' 8 | 9 | export function getFeed (feeds: ?Array, id: string): ?Feed { 10 | // console.log(feeds, id) 11 | // TODO: move use_extension to extension enabled?? 12 | const useMtc = getConfigProperty('modules.gtfsapi.use_extension') === 'mtc' 13 | const feed = feeds 14 | ? feeds.find( 15 | feed => 16 | useMtc 17 | ? objectPath.get(feed, 'externalProperties.MTC.AgencyId') === id 18 | : feed.id === id 19 | ) 20 | : null 21 | return feed 22 | } 23 | 24 | export function getFeedId (feed: ?Feed): ?string { 25 | const useMtc = getConfigProperty('modules.gtfsapi.use_extension') === 'mtc' 26 | return !feed 27 | ? null 28 | : useMtc ? objectPath.get(feed, 'externalProperties.MTC.AgencyId') : feed.id 29 | } 30 | 31 | function getRtdApi (): ?string { 32 | if (getConfigProperty('modules.alerts.use_extension') === 'mtc') { 33 | return getConfigProperty('extensions.mtc.rtd_api') 34 | } 35 | return null 36 | } 37 | 38 | export function getAlertsUrl (): string { 39 | const rtdApi = getRtdApi() 40 | return rtdApi ? rtdApi + '/ServiceAlert' : '/api/manager/secure/alerts' 41 | } 42 | 43 | export function getSignConfigUrl (): string { 44 | const rtdApi = getRtdApi() 45 | return rtdApi 46 | ? rtdApi + '/DisplayConfiguration' 47 | : '/api/manager/secure/displays' 48 | } 49 | 50 | export function getDisplaysUrl (): string { 51 | const rtdApi = getRtdApi() 52 | return rtdApi 53 | ? rtdApi + '/Display' 54 | : '/api/manager/secure/displays' 55 | } 56 | -------------------------------------------------------------------------------- /lib/common/util/permissions.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import {isModuleEnabled} from './config' 4 | 5 | import type {PermissionType} from '../user/UserPermissions' 6 | import type {AlertEntity, Feed, Project} from '../../types' 7 | import type {ManagerUserState} from '../../types/reducers' 8 | 9 | export function getFeedsForPermission ( 10 | project: ?Project, 11 | user: ManagerUserState, 12 | permission: PermissionType 13 | ): Array { 14 | if (project && project.feedSources) { 15 | const {id, organizationId} = project 16 | return project.feedSources.filter( 17 | feed => 18 | user.permissions && user.permissions.hasFeedPermission( 19 | organizationId, 20 | id, 21 | feed.id, 22 | permission 23 | ) !== null 24 | ) 25 | } 26 | return [] 27 | } 28 | 29 | // ensure list of feeds contains all agency IDs for set of entities 30 | export function checkEntitiesForFeeds ( 31 | entities: Array, 32 | feeds: Array 33 | ): boolean { 34 | const publishableIds: Array = feeds.map(f => f.id) 35 | const entityIds: Array = entities 36 | ? entities.map(entity => entity.agency ? entity.agency.id : '') 37 | : [] 38 | for (var i = 0; i < entityIds.length; i++) { 39 | if (publishableIds.indexOf(entityIds[i]) === -1) return false 40 | } 41 | return true 42 | } 43 | 44 | /** 45 | * Checks whether it is possible for a user in this application to analyze 46 | * project deployments 47 | */ 48 | export function deploymentsEnabledAndAccessAllowedForProject ( 49 | project: ?Project, 50 | user: ManagerUserState 51 | ): boolean { 52 | return !!project && 53 | isModuleEnabled('deployment') && 54 | !!user.permissions && 55 | user.permissions.isProjectAdmin(project.id, project.organizationId) 56 | } 57 | -------------------------------------------------------------------------------- /lib/common/util/text.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import toLower from 'lodash/toLower' 4 | import upperFirst from 'lodash/upperFirst' 5 | 6 | export default function toSentenceCase (s: string): string { 7 | return upperFirst(toLower(s)) 8 | } 9 | 10 | /** 11 | * This method takes a string like expires_in_7days and ensures 12 | * that 7days is replaced with 7 days 13 | */ 14 | // $FlowFixMe flow needs to learn about new es2021 features! 15 | export function spaceOutNumbers (s: string): string { 16 | return s.replaceAll('_', ' ') 17 | .split(/(?=[1-9])/) 18 | .join(' ') 19 | .toLowerCase() 20 | } 21 | -------------------------------------------------------------------------------- /lib/common/util/upload-file.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import fetch from 'isomorphic-fetch' 4 | 5 | import {getHeaders} from './util' 6 | 7 | export function uploadFile ({ 8 | file, token, url 9 | }: { 10 | file: File, token: string, url: string 11 | }) { 12 | return fetch(url, { 13 | method: 'post', 14 | headers: getHeaders(token, true, 'application/zip'), 15 | body: file 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /lib/common/util/user.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import objectPath from 'object-path' 4 | 5 | import type { AccountTypes, DatatoolsSettings, Profile, UserProfile } from '../../types' 6 | import type { AppState } from '../../types/reducers' 7 | 8 | /** 9 | * Predicate that determines whether a settings object correspond to the configured Auth0 client. 10 | */ 11 | export const isSettingForThisClient = (settings: DatatoolsSettings) => { 12 | const clientId = process.env.AUTH0_CLIENT_ID 13 | if (!clientId) throw new Error('Auth0 client ID must be set in config') 14 | 15 | return settings.client_id === clientId 16 | } 17 | 18 | /** 19 | * Obtains the entry for the user's app_metadata.datatools (in Auth0) that corresponds to the configured client, 20 | * or null if no such entry exists. 21 | */ 22 | export const getSettingsForThisClient = (allClientSettings?: ?Array): ?DatatoolsSettings => 23 | allClientSettings && allClientSettings.find(isSettingForThisClient) 24 | 25 | /** 26 | * Variant of above method that takes in a UserProfile object. 27 | */ 28 | export const getSettingsFromProfile = (profile: ?UserProfile): ?DatatoolsSettings => 29 | profile && 30 | profile.app_metadata && 31 | getSettingsForThisClient(profile.app_metadata.datatools) 32 | 33 | export const getUserMetadataProperty = ( 34 | profile: ?Profile, 35 | propertyString: string 36 | ) => { 37 | const datatools = objectPath.get(profile, 'user_metadata.datatools') 38 | const application = getSettingsForThisClient(datatools) 39 | return objectPath.get(application, propertyString) 40 | } 41 | 42 | /** 43 | * Gets the configured account types 44 | */ 45 | export function getAccountTypes (state: AppState): AccountTypes { 46 | const { appInfo } = state.status 47 | const { licensing } = (appInfo && appInfo.config.modules) || {} 48 | return (licensing && licensing.enabled && licensing.account_types) || {} 49 | } 50 | -------------------------------------------------------------------------------- /lib/editor/actions/map/__tests__/stopStrategies.js: -------------------------------------------------------------------------------- 1 | import clone from 'lodash/cloneDeep' 2 | 3 | import { updateShapeDistTraveled } from '../stopStrategies' 4 | 5 | const loopControlPoints = require('./fixtures/loop-ctrl-points.json') 6 | const loopPatternSegments = require('./fixtures/loop-pattern-segments.json') 7 | const loopPatternStops = require('./fixtures/loop-pattern-stops.json') 8 | 9 | describe('editor > actions > stopStrategies', () => { 10 | describe('updateShapeDistTraveled', () => { 11 | it('should populate distance traveled in a loop pattern', () => { 12 | const clonedPatternStops = clone(loopPatternStops) 13 | updateShapeDistTraveled(loopControlPoints, loopPatternSegments, clonedPatternStops) 14 | 15 | expect(clonedPatternStops[0].shapeDistTraveled).toBe(0) 16 | 17 | // Traveled distance should be in strict ascending order. 18 | for (let i = 1; i < clonedPatternStops.length; i++) { 19 | expect(clonedPatternStops[i].shapeDistTraveled).toBeGreaterThan(clonedPatternStops[i - 1].shapeDistTraveled) 20 | } 21 | 22 | // The pattern contains a loop, so the total distance traveled from pattern start 23 | // should be populated as opposed to the distance when hitting a repeated stop the first time. 24 | expect(clonedPatternStops[23].stopId).toBe(clonedPatternStops[0].stopId) 25 | expect(clonedPatternStops[23].shapeDistTraveled).toBe(184921.39475283914) 26 | 27 | expect(clonedPatternStops[21].stopId).toBe(clonedPatternStops[2].stopId) 28 | expect(clonedPatternStops[2].shapeDistTraveled).toBe(1621.2549997010212) 29 | expect(clonedPatternStops[21].shapeDistTraveled).toBe(183802.09138428463) 30 | 31 | // See other individual computed traveled distances in snapshot. 32 | expect(clonedPatternStops).toMatchSnapshot() 33 | }) 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /lib/editor/components/EditorSidebar.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React, {Component} from 'react' 4 | 5 | import ActiveSidebarNavItem from '../../common/containers/ActiveSidebarNavItem' 6 | import ActiveSidebar from '../../common/containers/ActiveSidebar' 7 | import {getComponentMessages} from '../../common/util/config' 8 | import { GTFS_ICONS } from '../util/ui' 9 | import type {Feed} from '../../types' 10 | import type {GtfsIcon} from '../util/ui' 11 | 12 | type Props = { 13 | activeComponent: string, 14 | editingIsDisabled: boolean, 15 | feedSource: Feed 16 | } 17 | 18 | export default class EditorSidebar extends Component { 19 | messages = getComponentMessages('EditorSidebar') 20 | 21 | isActive (item: GtfsIcon, component: string) { 22 | return component === item.id || (component === 'scheduleexception' && item.id === 'calendar') 23 | } 24 | 25 | render () { 26 | const {activeComponent, editingIsDisabled, feedSource} = this.props 27 | 28 | return ( 29 | 30 | 37 | {GTFS_ICONS.map(item => { 38 | return item.hideSidebar 39 | ? null 40 | : 52 | })} 53 | 54 | ) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/editor/components/ExceptionValidationErrorsList.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from 'react' 4 | 5 | import { getComponentMessages } from '../../common/util/config' 6 | import type { EditorValidationIssue } from '../util/validation' 7 | 8 | const VALIDATION_ERROR_LIMIT = 3 9 | 10 | const ExceptionValidationErrorsList = ({validationErrors}: {validationErrors: Array}) => { 11 | const messages = getComponentMessages('ExceptionValidationErrorsList') 12 | const excessValidationErrors = validationErrors.length - VALIDATION_ERROR_LIMIT 13 | 14 | return ( 15 |
    16 | {validationErrors.map((err, index) => { 17 | if (index < VALIDATION_ERROR_LIMIT) { 18 | return ( 19 |
  • 20 | {err.reason} 21 |
  • 22 | ) 23 | } 24 | })} 25 | {excessValidationErrors > 0 && ( 26 |
  • 27 | {messages('andOtherErrors').replace('%errors%', excessValidationErrors.toString())} 28 |
  • 29 | )} 30 |
31 | ) 32 | } 33 | 34 | export default ExceptionValidationErrorsList 35 | -------------------------------------------------------------------------------- /lib/editor/components/MinuteSecondInput.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React, {Component} from 'react' 4 | import {FormControl} from 'react-bootstrap' 5 | 6 | import { 7 | convertSecondsToMMSSString, 8 | convertMMSSStringToSeconds 9 | } from '../../common/util/date-time' 10 | 11 | import type {Style} from '../../types' 12 | 13 | type Props = { 14 | disabled?: boolean, 15 | onChange: number => void, 16 | seconds: number, 17 | style?: Style 18 | } 19 | 20 | type State = { 21 | seconds: number, 22 | string: string 23 | } 24 | 25 | const _getState = (seconds: number) => ({ 26 | seconds: typeof seconds === 'undefined' ? 0 : seconds, 27 | string: convertSecondsToMMSSString(seconds) 28 | }) 29 | 30 | export default class MinuteSecondInput extends Component { 31 | componentWillMount () { 32 | this.setState(_getState(this.props.seconds)) 33 | } 34 | 35 | componentWillReceiveProps (nextProps: Props) { 36 | if (typeof nextProps.seconds !== 'undefined' && this.state.seconds !== nextProps.seconds) { 37 | this.setState(_getState(nextProps.seconds)) 38 | } 39 | } 40 | 41 | _onChange = (evt: SyntheticInputEvent) => { 42 | const {onChange} = this.props 43 | const {value} = evt.target 44 | const seconds = convertMMSSStringToSeconds(value) 45 | if (seconds === this.state.seconds) { 46 | this.setState({string: value}) 47 | } else { 48 | this.setState({seconds, string: value}) 49 | onChange && onChange(seconds) 50 | } 51 | } 52 | 53 | render () { 54 | const {seconds, string} = this.state 55 | return ( 56 | 66 | ) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /lib/editor/components/map/AddableStop.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { divIcon } from 'leaflet' 4 | import React, {Component} from 'react' 5 | import {Marker, Popup} from 'react-leaflet' 6 | 7 | import * as stopStrategiesActions from '../../actions/map/stopStrategies' 8 | import AddPatternStopDropdown from '../pattern/AddPatternStopDropdown' 9 | 10 | import type {GtfsStop, Pattern} from '../../../types' 11 | 12 | type Props = { 13 | activePattern: Pattern, 14 | addStopToPattern: typeof stopStrategiesActions.addStopToPattern, 15 | stop: GtfsStop 16 | } 17 | 18 | export default class AddableStop extends Component { 19 | _onClickAddStopToEnd = () => { 20 | const {activePattern, addStopToPattern, stop} = this.props 21 | addStopToPattern(activePattern, stop) 22 | } 23 | 24 | _onSelectStop = (key: number) => 25 | this.props.addStopToPattern(this.props.activePattern, this.props.stop, key) 26 | 27 | render () { 28 | const { 29 | activePattern, 30 | addStopToPattern, 31 | stop 32 | } = this.props 33 | const color = 'blue' 34 | const stopName = `${stop.stop_name} (${stop.stop_code ? stop.stop_code : stop.stop_id})` 35 | const transparentBusIcon = divIcon({ 36 | html: ` 37 | 38 | 39 | `, 40 | className: '', 41 | iconSize: [24, 24] 42 | }) 43 | return ( 44 | 47 | 48 |
49 |
{stopName}
50 | 56 |
57 |
58 |
59 | ) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /lib/editor/components/pattern/NormalizeStopTimesTip.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import Icon from '@conveyal/woonerf/components/icon' 4 | import React, { Component } from 'react' 5 | 6 | import { getComponentMessages } from '../../../common/util/config' 7 | 8 | type Props = {} 9 | 10 | export default class NormalizeStopTimesTip extends Component { 11 | messages = getComponentMessages('NormalizeStopTimesTip') 12 | 13 | render () { 14 | return ( 15 |
16 | 17 | {this.messages('info')} 18 | 19 |
20 | ) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/editor/constants/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export const ENTITY = { 4 | // TODO: use these constants for component names 5 | STOP: 'stop', 6 | ROUTE: 'route', 7 | TRIP_PATTERN: 'trippattern', 8 | 9 | // For constructing new entities (before saving to server) 10 | NEW_ID: -2 11 | } 12 | 13 | export const POINT_TYPE = Object.freeze({ 14 | DEFAULT: 0, 15 | ANCHOR: 1, 16 | STOP: 2 17 | }) 18 | 19 | export const ARROW_MAGENTA = '#d50b65' 20 | 21 | export const PATTERN_TO_STOP_DISTANCE_THRESHOLD_METERS = 50 22 | -------------------------------------------------------------------------------- /lib/editor/containers/ActiveEditorFeedSourcePanel.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import {connect} from 'react-redux' 4 | 5 | import { 6 | deleteSnapshot, 7 | downloadSnapshot, 8 | fetchSnapshots, 9 | restoreSnapshot 10 | } from '../actions/snapshots.js' 11 | import {createFeedVersionFromSnapshot} from '../../manager/actions/versions' 12 | import EditorFeedSourcePanel from '../components/EditorFeedSourcePanel' 13 | import type {Feed, Project} from '../../types' 14 | import type {AppState} from '../../types/reducers' 15 | 16 | export type Props = { 17 | feedSource: Feed, 18 | project: Project 19 | } 20 | 21 | const mapStateToProps = (state: AppState, ownProps: Props) => { 22 | const {user} = state 23 | return { 24 | user 25 | } 26 | } 27 | 28 | const mapDispatchToProps = { 29 | createFeedVersionFromSnapshot, 30 | deleteSnapshot, 31 | downloadSnapshot, 32 | fetchSnapshots, 33 | restoreSnapshot 34 | } 35 | 36 | const ActiveEditorFeedSourcePanel = connect( 37 | mapStateToProps, 38 | mapDispatchToProps 39 | )(EditorFeedSourcePanel) 40 | 41 | export default ActiveEditorFeedSourcePanel 42 | -------------------------------------------------------------------------------- /lib/editor/containers/ActiveFeedInfoPanel.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import {connect} from 'react-redux' 4 | 5 | import {setActiveEntity} from '../actions/active' 6 | import {fetchSnapshots, restoreSnapshot} from '../actions/snapshots' 7 | import {displayRoutesShapefile} from '../actions/map' 8 | import FeedInfoPanel from '../components/FeedInfoPanel' 9 | import {findProjectByFeedSource} from '../../manager/util' 10 | import {getTableById} from '../util/gtfs' 11 | import type {AppState} from '../../types/reducers' 12 | 13 | export type Props = { 14 | feedSourceId: string, 15 | showConfirmModal: any 16 | } 17 | 18 | const mapStateToProps = (state: AppState, ownProps: Props) => { 19 | const {feedSourceId} = ownProps 20 | const project = findProjectByFeedSource(state.projects.all, feedSourceId) 21 | const feedSource = project && project.feedSources && project.feedSources.find(fs => fs.id === feedSourceId) 22 | const feedInfo = getTableById(state.editor.data.tables, 'feedinfo')[0] 23 | return { 24 | feedInfo, 25 | feedSourceId, 26 | feedSource, 27 | project 28 | } 29 | } 30 | 31 | const mapDispatchToProps = { 32 | displayRoutesShapefile, 33 | fetchSnapshots, 34 | restoreSnapshot, 35 | setActiveEntity 36 | } 37 | 38 | const ActiveFeedInfoPanel = connect(mapStateToProps, mapDispatchToProps)(FeedInfoPanel) 39 | 40 | export default ActiveFeedInfoPanel 41 | -------------------------------------------------------------------------------- /lib/editor/reducers/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { combineReducers } from 'redux' 4 | import {handleActions} from 'redux-actions' 5 | import undoable, {includeAction} from 'redux-undo' 6 | 7 | import data from './data' 8 | import * as settings from './settings' 9 | import * as mapState from './mapState' 10 | import timetable from './timetable' 11 | 12 | export default combineReducers({ 13 | data, 14 | editSettings: undoable( 15 | handleActions(settings.reducers, settings.defaultState), 16 | { undoType: 'UNDO_TRIP_PATTERN_EDITS', 17 | filter: includeAction(['UPDATE_PATTERN_GEOMETRY']), 18 | clearHistoryType: [ 19 | 'TOGGLE_PATTERN_EDITING', 20 | 'SAVED_TRIP_PATTERN' 21 | ], 22 | initialState: settings.defaultState 23 | } 24 | ), 25 | mapState: handleActions(mapState.reducers, mapState.defaultState), 26 | timetable 27 | }) 28 | -------------------------------------------------------------------------------- /lib/editor/reducers/mapState.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import update from 'react-addons-update' 4 | import {latLngBounds} from 'leaflet' 5 | import type {ActionType} from 'redux-actions' 6 | 7 | import {receivedRoutesShapefile, updateMapSetting} from '../actions/map' 8 | import {receiveFeedSource} from '../../manager/actions/feeds' 9 | import { getFeedBounds } from '../util/map' 10 | 11 | import type {MapState} from '../../types/reducers' 12 | 13 | export const defaultState = { 14 | zoom: null, 15 | bounds: latLngBounds([[60, 60], [-60, -20]]), // entire globe 16 | routesGeojson: null, 17 | target: null 18 | } 19 | 20 | export const reducers = { 21 | 'RECEIVE_FEEDSOURCE' ( 22 | state: MapState, 23 | action: ActionType 24 | ): MapState { 25 | if (action.payload) { 26 | return update(state, { 27 | bounds: {$set: latLngBounds(getFeedBounds(action.payload))}, 28 | target: {$set: action.payload && action.payload.id} 29 | }) 30 | } else { 31 | return state 32 | } 33 | }, 34 | 'RECEIVED_ROUTES_SHAPEFILE' ( 35 | state: MapState, 36 | action: ActionType 37 | ) { 38 | return update(state, { 39 | routesGeojson: {$set: action.payload.geojson} 40 | }) 41 | }, 42 | 'UPDATE_MAP_SETTING' ( 43 | state: MapState, 44 | action: ActionType 45 | ) { 46 | const updatedState = {} 47 | for (const key in action.payload) { 48 | if (key === 'bounds' && !action.payload[key]) { 49 | // do nothing. Setting bounds to null would cause an error for Leaflet 50 | } else { 51 | updatedState[key] = {$set: action.payload[key]} 52 | } 53 | } 54 | if (!('target' in action.payload)) { 55 | // If no target present in payload, set to null (no target to focus on) 56 | updatedState.target = {$set: null} 57 | } 58 | return update(state, updatedState) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/editor/util/__tests__/fixtures/graphhopper-response-delete-middle-control-point.json: -------------------------------------------------------------------------------- 1 | { 2 | "hints": { "visited_nodes.average": "10.0", "visited_nodes.sum": "10" }, 3 | "info": { 4 | "copyrights": ["GraphHopper", "OpenStreetMap contributors"], 5 | "took": 1 6 | }, 7 | "paths": [ 8 | { 9 | "distance": 426.705, 10 | "weight": 27.735793, 11 | "time": 21333, 12 | "transfers": 0, 13 | "points_encoded": true, 14 | "bbox": [-77.923058, 42.092351, -77.922526, 42.096168], 15 | "points": "ed|_GxfrzMuG^cNhA", 16 | "instructions": [ 17 | { 18 | "distance": 426.705, 19 | "heading": 355.28, 20 | "sign": 0, 21 | "interval": [0, 2], 22 | "text": "Continue onto Stannards Road, NY 19", 23 | "time": 21333, 24 | "street_name": "Stannards Road, NY 19" 25 | }, 26 | { 27 | "distance": 0.0, 28 | "sign": 4, 29 | "last_heading": 353.3201539453734, 30 | "interval": [2, 2], 31 | "text": "Arrive at destination", 32 | "time": 0, 33 | "street_name": "" 34 | } 35 | ], 36 | "legs": [], 37 | "details": {}, 38 | "ascend": 2.7185211181640625, 39 | "descend": 0.20001220703125, 40 | "snapped_waypoints": "ed|_GxfrzMyVhB" 41 | } 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /lib/editor/util/__tests__/fixtures/graphhopper-response-update-first-stop.json: -------------------------------------------------------------------------------- 1 | { 2 | "hints": { "visited_nodes.average": "18.0", "visited_nodes.sum": "18" }, 3 | "info": { 4 | "copyrights": ["GraphHopper", "OpenStreetMap contributors"], 5 | "took": 1 6 | }, 7 | "paths": [ 8 | { 9 | "distance": 614.966, 10 | "weight": 64.249833, 11 | "time": 55023, 12 | "transfers": 0, 13 | "points_encoded": true, 14 | "bbox": [-77.92553, 42.086499, -77.921839, 42.089254], 15 | "points": "{`{_GpyrzM?oJBiABWVw@BKDiCCaAcP~@", 16 | "instructions": [ 17 | { 18 | "distance": 308.98, 19 | "heading": 90.39, 20 | "sign": 0, 21 | "interval": [0, 7], 22 | "text": "Continue onto Moonlight Drive", 23 | "time": 39725, 24 | "street_name": "Moonlight Drive" 25 | }, 26 | { 27 | "distance": 305.986, 28 | "sign": -2, 29 | "interval": [7, 8], 30 | "text": "Turn left onto Stannards Road, NY 19", 31 | "time": 15298, 32 | "street_name": "Stannards Road, NY 19" 33 | }, 34 | { 35 | "distance": 0.0, 36 | "sign": 4, 37 | "last_heading": 354.6150385623691, 38 | "interval": [8, 8], 39 | "text": "Arrive at destination", 40 | "time": 0, 41 | "street_name": "" 42 | } 43 | ], 44 | "legs": [], 45 | "details": {}, 46 | "ascend": 3.107513427734375, 47 | "descend": 3.8070068359375, 48 | "snapped_waypoints": "{`{_GpyrzM}NaT" 49 | } 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /lib/editor/util/__tests__/fixtures/graphhopper-response-update-last-stop.json: -------------------------------------------------------------------------------- 1 | { 2 | "hints": { "visited_nodes.average": "8.0", "visited_nodes.sum": "8" }, 3 | "info": { 4 | "copyrights": ["GraphHopper", "OpenStreetMap contributors"], 5 | "took": 1 6 | }, 7 | "paths": [ 8 | { 9 | "distance": 421.778, 10 | "weight": 27.415538, 11 | "time": 21087, 12 | "transfers": 0, 13 | "points_encoded": true, 14 | "bbox": [-77.923636, 42.094262, -77.922759, 42.097986], 15 | "points": "cp|_GfhrzMyLdAeBViAV}Bx@", 16 | "instructions": [ 17 | { 18 | "distance": 421.778, 19 | "heading": 353.6, 20 | "sign": 0, 21 | "interval": [0, 4], 22 | "text": "Continue onto Stannards Road, NY 19", 23 | "time": 21087, 24 | "street_name": "Stannards Road, NY 19" 25 | }, 26 | { 27 | "distance": 0.0, 28 | "sign": 4, 29 | "last_heading": 338.663893545927, 30 | "interval": [4, 4], 31 | "text": "Arrive at destination", 32 | "time": 0, 33 | "street_name": "" 34 | } 35 | ], 36 | "legs": [], 37 | "details": {}, 38 | "ascend": 2.47503662109375, 39 | "descend": 1.240020751953125, 40 | "snapped_waypoints": "cp|_GfhrzMgVnD" 41 | } 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /lib/editor/util/__tests__/fixtures/test-control-points.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "distance": 0, 3 | "id": "bv67", 4 | "pointType": 2, 5 | "shapePtSequence": 0, 6 | "point": { 7 | "type": "Feature", 8 | "properties": {}, 9 | "geometry": { 10 | "type": "Point", 11 | "coordinates": [-77.922203, 42.086407] 12 | } 13 | }, 14 | "stopId": "56c69e57-b007-4367-94ed-2c224662a2af" 15 | }, { 16 | "distance": 345.8645390523855, 17 | "id": "y1e5", 18 | "pointType": 1, 19 | "shapePtSequence": 1, 20 | "point": { 21 | "type": "Feature", 22 | "properties": {}, 23 | "geometry": { 24 | "type": "Point", 25 | "coordinates": [-77.92215494185281, 42.08925407008939] 26 | } 27 | } 28 | }, { 29 | "distance": 691.729078104771, 30 | "id": "fyz2", 31 | "pointType": 2, 32 | "shapePtSequence": 2, 33 | "point": { 34 | "type": "Feature", 35 | "properties": {}, 36 | "geometry": { 37 | "type": "Point", 38 | "coordinates": [-77.92252574804179, 42.09235127309673] 39 | } 40 | }, 41 | "stopId": "a9c9cd55-ca3f-4ce7-9a93-96f2f4eaf089" 42 | }, { 43 | "distance": 905.1384059344052, 44 | "id": "0cj3", 45 | "pointType": 1, 46 | "shapePtSequence": 3, 47 | "point": { 48 | "type": "Feature", 49 | "properties": {}, 50 | "geometry": { 51 | "type": "Point", 52 | "coordinates": [-77.92276015989871, 42.09426187381858] 53 | } 54 | } 55 | }, { 56 | "distance": 1118.5477337640395, 57 | "id": "1gwo", 58 | "pointType": 2, 59 | "shapePtSequence": 4, 60 | "point": { 61 | "type": "Feature", 62 | "properties": {}, 63 | "geometry": { 64 | "type": "Point", 65 | "coordinates": [-77.92305462145765, 42.096168018547736] 66 | } 67 | }, 68 | "stopId": "5b6afe4d-f4f2-43f3-810e-91749420af53" 69 | }] 70 | -------------------------------------------------------------------------------- /lib/editor/util/__tests__/fixtures/test-pattern-shape.json: -------------------------------------------------------------------------------- 1 | [ 2 | [ 3 | [-77.922203, 42.086407], 4 | [-77.92215494185281, 42.08925407008939] 5 | ], 6 | [ 7 | [-77.92215494185281, 42.08925407008939], 8 | [-77.92252574804179, 42.09235127309673] 9 | ], 10 | [ 11 | [-77.92252574804179, 42.09235127309673], 12 | [-77.92276015989871, 42.09426187381858] 13 | ], 14 | [ 15 | [-77.92276015989871, 42.09426187381858], 16 | [-77.92305462145765, 42.096168018547736] 17 | ] 18 | ] 19 | -------------------------------------------------------------------------------- /lib/editor/util/__tests__/gtfs.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import {getEntityName} from '../gtfs' 4 | 5 | describe('editor > util > gtfs', () => { 6 | describe('getEntityName', () => { 7 | describe('routes', () => { 8 | it('should get name for route with no names', () => { 9 | // cast input to any flow type cause I'm lazy and don't want to 10 | // add all variables when they aren't needed 11 | expect(getEntityName(({ 12 | route_id: '1' 13 | }: any))).toEqual('[no name]') 14 | }) 15 | 16 | it('should get name for route with just short name', () => { 17 | // cast input to any flow type cause I'm lazy and don't want to 18 | // add all variables when they aren't needed 19 | expect(getEntityName(({ 20 | route_id: '1', 21 | route_short_name: 'short name' 22 | }: any))).toEqual('short name') 23 | }) 24 | 25 | it('should get name for route with just long name', () => { 26 | // cast input to any flow type cause I'm lazy and don't want to 27 | // add all variables when they aren't needed 28 | expect(getEntityName(({ 29 | route_id: '1', 30 | route_long_name: 'long name' 31 | }: any))).toEqual('long name') 32 | }) 33 | 34 | it('should get name for route with both short and long name', () => { 35 | // cast input to any flow type cause I'm lazy and don't want to 36 | // add all variables when they aren't needed 37 | expect(getEntityName(({ 38 | route_id: '1', 39 | route_long_name: 'long name', 40 | route_short_name: 'short name' 41 | }: any))).toEqual('short name - long name') 42 | }) 43 | }) 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /lib/editor/util/debug.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import featurecollection from 'turf-featurecollection' 4 | import point from 'turf-point' 5 | 6 | import type {Coordinates, GeoJsonFeatureCollection} from '../../types' 7 | 8 | /** 9 | * Log a link to the input feature collection rendered in geojson.io 10 | */ 11 | export function logGeojsonioUrl (features: GeoJsonFeatureCollection) { 12 | console.log(`http://geojson.io/#data=data:application/json,${encodeURIComponent(JSON.stringify(features))}`) 13 | } 14 | 15 | /** 16 | * Convert array of coordinates to a feature collection. 17 | */ 18 | export function logCoordsToGeojsonio (coords: Coordinates) { 19 | const features = coordsToFeatureCollection(coords) 20 | logGeojsonioUrl(features) 21 | } 22 | 23 | function coordsToFeatureCollection (coords: Coordinates): GeoJsonFeatureCollection { 24 | // Feature collection used for debug logging to geojson.io 25 | return featurecollection(coords.map(c => point(c))) 26 | } 27 | -------------------------------------------------------------------------------- /lib/editor/util/types.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export const FIELD_PROPS = [ 4 | { 5 | inputType: 'DROPDOWN', 6 | props: { 7 | componentClass: 'select' 8 | } 9 | }, 10 | { 11 | inputType: 'NUMBER', 12 | props: { 13 | type: 'number' 14 | } 15 | }, 16 | { 17 | inputType: 'POSITIVE_INT', 18 | props: { 19 | min: 0, 20 | step: 1, 21 | type: 'number' 22 | } 23 | }, 24 | { 25 | inputType: 'POSITIVE_NUM', 26 | props: { 27 | min: 0, 28 | type: 'number' 29 | } 30 | }, 31 | { 32 | inputType: 'TIME', 33 | props: { 34 | placeholder: 'HH:MM:SS' 35 | } 36 | }, 37 | { 38 | inputType: 'LATITUDE', 39 | props: { 40 | type: 'number' 41 | } 42 | }, 43 | { 44 | inputType: 'LONGITUDE', 45 | props: { 46 | type: 'number' 47 | } 48 | } 49 | ] 50 | -------------------------------------------------------------------------------- /lib/gtfs/actions/shapes.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import {createAction, type ActionType} from 'redux-actions' 4 | 5 | import {createVoidPayloadAction, fetchGraphQL} from '../../common/actions' 6 | import {decodeShapePolylines} from '../../common/util/gtfs' 7 | import {shapes} from '../../gtfs/util/graphql' 8 | import {updateRoutesOnMapDisplay} from './filter' 9 | 10 | import type {dispatchFn, getStateFn} from '../../types/reducers' 11 | 12 | export const errorFetchingShapes = createVoidPayloadAction( 13 | 'FETCH_GRAPHQL_SHAPES_REJECTED' 14 | ) 15 | export const fetchingShapes = createVoidPayloadAction('FETCH_GRAPHQL_SHAPES') 16 | export const receiveShapes = createAction( 17 | 'FETCH_GRAPHQL_SHAPES_FULFILLED', 18 | (payload: Array<[number, number]>) => payload 19 | ) 20 | 21 | export type GtfsShapesActions = ActionType | 22 | ActionType | 23 | ActionType 24 | 25 | export function toggleShowAllRoutesOnMap (namespace: string) { 26 | return function (dispatch: dispatchFn, getState: getStateFn) { 27 | let state = getState() 28 | dispatch(updateRoutesOnMapDisplay(!state.gtfs.filter.showAllRoutesOnMap)) 29 | state = getState() 30 | if ( 31 | state.gtfs.filter.showAllRoutesOnMap && 32 | !state.gtfs.shapes.fetchStatus.fetched && 33 | !state.gtfs.shapes.fetchStatus.fetching 34 | ) { 35 | dispatch(fetchingShapes()) 36 | return dispatch(fetchGraphQL({query: shapes, variables: {namespace}})) 37 | .then(data => { 38 | const {shapes_as_polylines: encodedShapes} = data.feed 39 | const shapes = encodedShapes.map(decodeShapePolylines) 40 | dispatch(receiveShapes(shapes)) 41 | }) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/gtfs/containers/GlobalGtfsFilter.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { connect } from 'react-redux' 4 | 5 | import GtfsFilter from '../components/GtfsFilter' 6 | import { 7 | addActiveFeed, 8 | removeActiveFeed, 9 | addAllActiveFeeds, 10 | removeAllActiveFeeds 11 | } from '../actions/filter' 12 | import {getActiveProject} from '../../manager/selectors' 13 | import {getActiveFeeds, getPublishedFeeds, getActiveAndLoadedFeeds, getAllFeeds} from '../../gtfs/selectors' 14 | 15 | import type {AppState} from '../../types/reducers' 16 | 17 | export type Props = {} 18 | 19 | const mapStateToProps = (state: AppState, ownProps: Props) => { 20 | return { 21 | activeFeeds: getActiveFeeds(state), 22 | allFeeds: getAllFeeds(state), 23 | activeAndLoadedFeeds: getActiveAndLoadedFeeds(state), 24 | loadedFeeds: getPublishedFeeds(state), 25 | user: state.user, 26 | project: getActiveProject(state) 27 | } 28 | } 29 | 30 | const mapDispatchToProps = { 31 | addActiveFeed, 32 | addAllActiveFeeds, 33 | removeActiveFeed, 34 | removeAllActiveFeeds 35 | } 36 | 37 | const GlobalGtfsFilter = connect(mapStateToProps, mapDispatchToProps)(GtfsFilter) 38 | 39 | export default GlobalGtfsFilter 40 | -------------------------------------------------------------------------------- /lib/gtfs/containers/ShowAllRoutesOnMapFilter.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import {connect} from 'react-redux' 4 | 5 | import {toggleShowAllRoutesOnMap} from '../actions/shapes' 6 | import ShowAllRoutesOnMapFilter from '../components/ShowAllRoutesOnMapFilter' 7 | 8 | import type {FeedVersion} from '../../types' 9 | import type {AppState} from '../../types/reducers' 10 | 11 | export type Props = {version: FeedVersion} 12 | 13 | const mapStateToProps = (state: AppState, ownProps: Props) => { 14 | return { 15 | fetchStatus: state.gtfs.shapes.fetchStatus, 16 | showAllRoutesOnMap: state.gtfs.filter.showAllRoutesOnMap 17 | } 18 | } 19 | 20 | const mapDispatchToProps = { 21 | toggleShowAllRoutesOnMap 22 | } 23 | 24 | export default connect( 25 | mapStateToProps, 26 | mapDispatchToProps 27 | )(ShowAllRoutesOnMapFilter) 28 | -------------------------------------------------------------------------------- /lib/gtfs/reducers/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { combineReducers } from 'redux' 4 | 5 | import filter from './filter' 6 | import patterns from './patterns' 7 | import routes from './routes' 8 | import shapes from './shapes' 9 | import stops from './stops' 10 | import timetables from './timetables' 11 | import validation from './validation' 12 | 13 | export default combineReducers({ 14 | filter, 15 | patterns, 16 | routes, 17 | shapes, 18 | stops, 19 | timetables, 20 | validation 21 | }) 22 | -------------------------------------------------------------------------------- /lib/gtfs/reducers/shapes.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type {Action} from '../../types/actions' 4 | import type {ShapesState} from '../../types/reducers' 5 | 6 | export const defaultState = { 7 | fetchStatus: { 8 | fetched: false, 9 | fetching: false, 10 | error: false 11 | }, 12 | data: [] 13 | } 14 | 15 | export default function reducer ( 16 | state: ShapesState = defaultState, 17 | action: Action 18 | ): ShapesState { 19 | switch (action.type) { 20 | case 'SET_ACTIVE_FEEDVERSION': 21 | return defaultState 22 | case 'FETCH_GRAPHQL_SHAPES': 23 | return { 24 | fetchStatus: { 25 | fetched: false, 26 | fetching: true, 27 | error: false 28 | }, 29 | data: [] 30 | } 31 | case 'FETCH_GRAPHQL_SHAPES_REJECTED': 32 | return { 33 | fetchStatus: { 34 | fetched: false, 35 | fetching: false, 36 | error: true 37 | }, 38 | data: [] 39 | } 40 | case 'FETCH_GRAPHQL_SHAPES_FULFILLED': 41 | return { 42 | fetchStatus: { 43 | fetched: true, 44 | fetching: false, 45 | error: false 46 | }, 47 | data: action.payload 48 | } 49 | default: 50 | return state 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/gtfs/reducers/stops.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type {Action} from '../../types/actions' 4 | import type {StopsState} from '../../types/reducers' 5 | 6 | export const defaultState = { 7 | routeFilter: null, 8 | patternFilter: null, 9 | fetchStatus: { 10 | fetched: false, 11 | fetching: false, 12 | error: false 13 | }, 14 | data: [] 15 | } 16 | 17 | export default function reducer ( 18 | state: StopsState = defaultState, 19 | action: Action 20 | ): StopsState { 21 | switch (action.type) { 22 | case 'SET_ACTIVE_FEEDVERSION': 23 | return defaultState 24 | case 'FETCH_GRAPHQL_STOPS': 25 | return { 26 | ...state, 27 | fetchStatus: { 28 | fetched: false, 29 | fetching: true, 30 | error: false 31 | }, 32 | data: [] 33 | } 34 | case 'FETCH_GRAPHQL_STOPS_REJECTED': 35 | return { 36 | ...state, 37 | fetchStatus: { 38 | fetched: false, 39 | fetching: false, 40 | error: true 41 | }, 42 | data: [] 43 | } 44 | case 'RECEIVED_GTFS_ELEMENTS': 45 | return { 46 | ...state, 47 | fetchStatus: { 48 | fetched: true, 49 | fetching: false, 50 | error: false 51 | }, 52 | data: action.payload.stops 53 | } 54 | default: 55 | return state 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/gtfs/reducers/timetables.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type {Action} from '../../types/actions' 4 | import type {TimetablesState} from '../../types/reducers' 5 | 6 | export const defaultState = { 7 | fetchStatus: { 8 | fetched: false, 9 | fetching: false, 10 | error: false 11 | }, 12 | data: null 13 | } 14 | 15 | export default function reducer ( 16 | state: TimetablesState = defaultState, 17 | action: Action 18 | ): TimetablesState { 19 | switch (action.type) { 20 | case 'SET_ACTIVE_FEEDVERSION': 21 | return defaultState 22 | case 'FETCH_GRAPHQL_TIMETABLES': 23 | return { 24 | fetchStatus: { 25 | fetched: false, 26 | fetching: true, 27 | error: false 28 | }, 29 | data: null 30 | } 31 | case 'FETCH_GRAPHQL_TIMETABLES_REJECTED': 32 | return { 33 | fetchStatus: { 34 | fetched: false, 35 | fetching: false, 36 | error: true 37 | }, 38 | data: null 39 | } 40 | case 'FETCH_GRAPHQL_TIMETABLES_FULFILLED': 41 | const {data} = action.payload 42 | 43 | return { 44 | fetchStatus: { 45 | fetched: true, 46 | fetching: false, 47 | error: false 48 | }, 49 | data 50 | } 51 | default: 52 | return state 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/gtfs/reducers/validation.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import update from 'react-addons-update' 4 | 5 | import {getEntityGraphQLRoot, getEntityIdField} from '../../gtfs/util' 6 | 7 | import type {Action} from '../../types/actions' 8 | import type {ValidationState} from '../../types/reducers' 9 | 10 | export const defaultState = { 11 | fetchStatus: { 12 | fetched: false, 13 | fetching: false, 14 | error: false 15 | }, 16 | data: {} 17 | } 18 | 19 | export default function reducer ( 20 | state: ValidationState = defaultState, 21 | action: Action 22 | ): ValidationState { 23 | switch (action.type) { 24 | case 'SET_ACTIVE_FEEDVERSION': 25 | return defaultState 26 | // TODO: Refactor how validation data is stored for feed version? Currently, 27 | // this is stored in the projects reducer (project.feedSource.feedVersion), 28 | // which is a bit cumbersome, but would require quite a bit of effort to 29 | // refactor. 30 | // case 'RECEIVE_VALIDATION_ISSUE_COUNT': { 31 | // const {feedVersion, validationIssueCount} = action.payload 32 | // return update(state, { 33 | // validationIssueCount: {$set: validationIssueCount} 34 | // }) 35 | // } 36 | case 'RECEIVE_GTFS_ENTITIES': 37 | const {data, editor, component} = action.payload 38 | if (editor) { 39 | // Ignore entity fetches for the editor 40 | return state 41 | } 42 | const entities = data.feed[getEntityGraphQLRoot(component)] 43 | const lookup = {} 44 | entities.forEach(entity => { 45 | const id = entity[getEntityIdField(component)] 46 | lookup[`${component}:${id}`] = entity 47 | }) 48 | return update(state, { 49 | data: {$merge: lookup} 50 | }) 51 | default: 52 | return state 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/gtfs/selectors/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { createSelector } from 'reselect' 4 | 5 | import {getFeedsForPermission} from '../../common/util/permissions' 6 | import {getActiveProject} from '../../manager/selectors' 7 | 8 | import type {AppState} from '../../types/reducers' 9 | 10 | const getPermissionFilter = (state: AppState) => state.gtfs.filter.permissionFilter 11 | 12 | const getUser = (state: AppState) => state.user 13 | 14 | export const getAllFeeds: AppState => any = createSelector( 15 | [ getUser, getPermissionFilter, getActiveProject ], 16 | (user, filter, project) => { 17 | return getFeedsForPermission(project, user, filter) 18 | } 19 | ) 20 | 21 | export const getPublishedFeeds = createSelector( 22 | [ getActiveProject ], 23 | (activeProject) => { 24 | return activeProject && activeProject.feedSources 25 | ? activeProject.feedSources.filter(feedSource => feedSource.publishedVersionId) 26 | : [] 27 | } 28 | ) 29 | 30 | export const getActiveFeeds: AppState => any = createSelector( 31 | [ getAllFeeds, state => state.gtfs.filter.activeFeeds ], 32 | (all, active) => { 33 | return all.filter((feed, index) => active && active[feed.id]) 34 | } 35 | ) 36 | 37 | export const getActiveAndLoadedFeeds: AppState => any = createSelector( 38 | [ getActiveFeeds, getPublishedFeeds ], 39 | (active, published) => { 40 | return active.filter(f => f && published.findIndex(feed => feed.id === f.id) !== -1) 41 | } 42 | ) 43 | -------------------------------------------------------------------------------- /lib/gtfs/util/__tests__/stats.js: -------------------------------------------------------------------------------- 1 | /* globals describe, expect, it */ 2 | 3 | import * as stats from '../stats' 4 | 5 | describe('gtfs > util > stats', () => { 6 | it('formatSpeed should work', () => { 7 | expect(stats.formatSpeed(123)).toEqual('275') 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /lib/gtfs/util/stats.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export function formatHeadway (seconds: number): string { 4 | if (seconds > 0) { 5 | return '' + Math.round(seconds / 60) 6 | } else if (seconds === 0) { 7 | return '0' 8 | } else { 9 | return 'N/A' 10 | } 11 | } 12 | 13 | export function formatSpeed (metersPerSecond: number): string { 14 | return metersPerSecond >= 0 ? '' + Math.round(metersPerSecond * 2.236936) : 'N/A' 15 | } 16 | -------------------------------------------------------------------------------- /lib/gtfsplus/components/GtfsPlusFieldHeader.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React, {Component} from 'react' 4 | import {Glyphicon, OverlayTrigger, Tooltip} from 'react-bootstrap' 5 | 6 | import type {GtfsSpecField, GtfsSpecTable, GtfsPlusValidationIssue} from '../../types' 7 | 8 | type Props = { 9 | field: GtfsSpecField, 10 | showHelpClicked: (string, ?string) => void, 11 | table: GtfsSpecTable, 12 | tableValidation: Array 13 | } 14 | 15 | export default class GtfsPlusFieldHeader extends Component { 16 | _onClick = () => { 17 | this.props.showHelpClicked(this.props.table.id, this.props.field.name) 18 | } 19 | 20 | /** 21 | * Column structure issues are identified by not having -1 for row index (not 22 | * applicable to any data row) and matching the field name. 23 | */ 24 | _getColumnStructureIssues = () => { 25 | const {field, tableValidation} = this.props 26 | const tableLevelIssues = tableValidation.filter(issue => 27 | issue.rowIndex === -1 && issue.fieldName === field.name) 28 | return tableLevelIssues.length > 0 ? tableLevelIssues : null 29 | } 30 | 31 | render () { 32 | const {field} = this.props 33 | const columnIssues = this._getColumnStructureIssues() 34 | const tooltip = columnIssues 35 | ? 36 | {columnIssues[0].description} 37 | 38 | : null 39 | return ( 40 | 42 | {field.name}{field.required ? ' *' : ''} 43 | {columnIssues && 44 | 45 | 46 | 47 | } 48 | 53 | 54 | ) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/gtfsplus/containers/ActiveGtfsPlusVersionSummary.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import {connect} from 'react-redux' 4 | 5 | import GtfsPlusVersionSummary from '../components/GtfsPlusVersionSummary' 6 | import {deleteGtfsPlusFeed, downloadGtfsPlusFeed, publishGtfsPlusFeed} from '../actions/gtfsplus' 7 | import {getValidationIssuesForTable} from '../selectors' 8 | import type {FeedVersion, FeedVersionSummary} from '../../types' 9 | import type {AppState} from '../../types/reducers' 10 | 11 | export type Props = { 12 | version: FeedVersion, 13 | versionSummaries?: Array 14 | } 15 | 16 | const mapStateToProps = (state: AppState, ownProps: Props) => { 17 | return { 18 | gtfsplus: state.gtfsplus, 19 | issuesForTable: getValidationIssuesForTable(state), 20 | user: state.user 21 | } 22 | } 23 | 24 | const mapDispatchToProps = { 25 | deleteGtfsPlusFeed, 26 | downloadGtfsPlusFeed, 27 | publishGtfsPlusFeed 28 | } 29 | 30 | const ActiveGtfsPlusVersionSummary = connect( 31 | mapStateToProps, 32 | mapDispatchToProps 33 | )(GtfsPlusVersionSummary) 34 | 35 | export default ActiveGtfsPlusVersionSummary 36 | -------------------------------------------------------------------------------- /lib/gtfsplus/reducers/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import gtfsplus from './gtfsplus' 4 | 5 | export default { gtfsplus } 6 | -------------------------------------------------------------------------------- /lib/gtfsplus/util/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import {getGtfsPlusSpec} from '../../common/util/config' 4 | 5 | export function constructNewGtfsPlusRow (tableId: string): { [string]: any } { 6 | const table = getGtfsPlusSpec() 7 | .find(t => t.id === tableId) 8 | if (typeof table === 'undefined') { 9 | throw new Error(`Could not find table '${tableId}' in GTFS+ spec`) 10 | } 11 | const rowData = {} 12 | for (const field of table.fields) { 13 | rowData[field.name] = null 14 | } 15 | return rowData 16 | } 17 | -------------------------------------------------------------------------------- /lib/index.css: -------------------------------------------------------------------------------- 1 | @import url(node_modules/font-awesome/css/font-awesome.css); 2 | /** 3 | * Note: this css file must be imported so that marker icons URLs will work 4 | * properly with react-leaflet. See https://github.com/PaulLeCam/react-leaflet/issues/453 5 | */ 6 | @import url(https://unpkg.com/leaflet@1.7.1/dist/leaflet.css); 7 | 8 | @import url(node_modules/bootstrap/dist/css/bootstrap.min.css); 9 | @import url(node_modules/react-bootstrap-table/dist/react-bootstrap-table.min.css); 10 | @import url(node_modules/react-bootstrap-datetimepicker/css/bootstrap-datetimepicker.min.css); 11 | 12 | @import url(node_modules/react-select/dist/react-select.css); 13 | 14 | @import url(node_modules/react-virtualized/styles.css); 15 | @import url(node_modules/react-virtualized-select/styles.css); 16 | @import url(node_modules/rc-slider/assets/index.css); 17 | @import url(node_modules/react-toggle/style.css); 18 | @import url(node_modules/react-toastify/dist/ReactToastify.min.css); 19 | @import url(node_modules/react-dropdown-tree-select/dist/styles.css); 20 | 21 | @import url(lib/style.css); 22 | -------------------------------------------------------------------------------- /lib/main.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import mount from '@conveyal/woonerf/mount' 4 | 5 | import admin from './admin/reducers' 6 | import alerts from './alerts/reducers' 7 | import App from './common/containers/App' 8 | import editor from './editor/reducers' 9 | import gtfs from './gtfs/reducers' 10 | import * as gtfsPlusReducers from './gtfsplus/reducers' 11 | import * as managerReducers from './manager/reducers' 12 | 13 | mount({ 14 | app: App, 15 | reducers: { 16 | ...managerReducers, 17 | admin, 18 | alerts, 19 | ...gtfsPlusReducers, 20 | editor, 21 | gtfs 22 | } 23 | }) 24 | -------------------------------------------------------------------------------- /lib/manager/actions/__tests__/__snapshots__/user.js.snap.hold: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`manager > actions > user > receiveTokenAndProfile should receive token and profile 1`] = ` 4 | Object { 5 | "payload": Object { 6 | "permissions": UserPermissions { 7 | "appPermissionLookup": Object {}, 8 | "orgPermissionLookup": Object {}, 9 | "organizationLookup": Object {}, 10 | "projectLookup": Object {}, 11 | }, 12 | "profile": Object { 13 | "app_metadata": Object { 14 | "datatools": Array [], 15 | }, 16 | }, 17 | "token": "fake-token", 18 | }, 19 | "type": "USER_LOGGED_IN", 20 | } 21 | `; 22 | -------------------------------------------------------------------------------- /lib/manager/actions/__tests__/projects.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import nock from 'nock' 4 | 5 | import mockData from '../../../../__tests__/test-utils/mock-data' 6 | import * as projectsActions from '../projects' 7 | 8 | const { 9 | getMockInitialState, 10 | getMockStateWithAdminUser, 11 | makeMockStore 12 | } = mockData.store 13 | 14 | describe('manager > actions > projects >', () => { 15 | it('should set a new feed source sort order', () => { 16 | const store = makeMockStore(getMockInitialState()) 17 | store.dispatch(projectsActions.setFeedSort('alphabetically-desc')) 18 | store.expectStateToMatchSnapshot('projects') 19 | }) 20 | 21 | it('should load a project and associated data', async () => { 22 | const projectId = 'mock-project-with-deployments-id' 23 | const serverUrl = 'http://localhost:4000' 24 | // mock for fetching project 25 | nock(serverUrl) 26 | .get(`/api/manager/secure/project/${projectId}`) 27 | .reply(200, mockData.manager.mockProjectWithDeploymentUnloaded) 28 | // mock for fetching project feeds 29 | .get(`/api/manager/secure/feedsourceSummaries?projectId=${projectId}`) 30 | .reply(200, [mockData.manager.mockFeedSourceSummaryWithVersion]) 31 | // mock for fetching the project deployments 32 | .get(`/api/manager/secure/deployments?projectId=${projectId}`) 33 | .reply(200, [mockData.manager.mockDeployment]) 34 | .get(`/api/manager/secure/deploymentSummaries?projectId=${projectId}`) 35 | .reply(200, [mockData.manager.mockDeploymentSummary]) 36 | 37 | const store = makeMockStore(getMockStateWithAdminUser()) 38 | await store.dispatch( 39 | projectsActions.onProjectViewerMount(projectId) 40 | ) 41 | store.expectStateToMatchSnapshot('projects') 42 | }) 43 | }) 44 | -------------------------------------------------------------------------------- /lib/manager/actions/__tests__/user.js.hold: -------------------------------------------------------------------------------- 1 | import {receiveTokenAndProfile} from '../user' 2 | 3 | describe('manager > actions > user > ', () => { 4 | describe('receiveTokenAndProfile', () => { 5 | it('should receive token and profile', () => { 6 | expect( 7 | receiveTokenAndProfile({ 8 | token: 'fake-token', 9 | profile: { 10 | app_metadata: { 11 | datatools: [] 12 | } 13 | } 14 | }) 15 | ).toMatchSnapshot() 16 | }) 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /lib/manager/actions/languages.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import {createAction, type ActionType} from 'redux-actions' 4 | 5 | export const setActiveLanguage = createAction( 6 | 'SET_ACTIVE_LANGUAGE', 7 | (payload: string) => payload 8 | ) 9 | 10 | export type LanguageActions = ActionType 11 | -------------------------------------------------------------------------------- /lib/manager/actions/visibilityFilter.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { createAction, type ActionType } from 'redux-actions' 4 | 5 | export const setVisibilitySearchText = createAction( 6 | 'SET_PROJECT_VISIBILITY_SEARCH_TEXT', 7 | (payload: null | string) => payload 8 | ) 9 | export const setVisibilityLabel = createAction( 10 | 'SET_PROJECT_VISIBILITY_LABEL', 11 | (payload: Array) => payload 12 | ) 13 | export const setVisibilityLabelMode = createAction( 14 | 'SET_PROJECT_VISIBILITY_LABEL_MODE', 15 | (payload: string) => payload 16 | ) 17 | export const setVisibilityFilter = createAction( 18 | 'SET_PROJECT_VISIBILITY_FILTER', 19 | (payload: any) => payload 20 | ) 21 | 22 | export type VisibilityFilterActions = ActionType | 23 | ActionType 24 | -------------------------------------------------------------------------------- /lib/manager/components/CreateProject.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React, { Component } from 'react' 4 | import { Col, Grid, Row } from 'react-bootstrap' 5 | 6 | import { createProject } from '../actions/projects' 7 | import ManagerPage from '../../common/components/ManagerPage' 8 | import {getComponentMessages} from '../../common/util/config' 9 | import type { ManagerUserState } from '../../types/reducers' 10 | 11 | import ProjectSettingsForm from './ProjectSettingsForm' 12 | 13 | type Props = { 14 | createProject: typeof createProject, 15 | user: ManagerUserState 16 | } 17 | 18 | /** 19 | * A component to facilitate the creation of a new project. 20 | */ 21 | export default class CreateProject extends Component { 22 | messages = getComponentMessages('CreateProject') 23 | 24 | _saveProject = (projectId: string, data: Object) => { 25 | return this.props.createProject(data) 26 | } 27 | 28 | render () { 29 | const { user } = this.props 30 | 31 | return ( 32 | 35 | 36 | 37 | 38 |

{this.messages('new')}

39 | 46 | 47 |
48 |
49 |
50 | ) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/manager/components/LabelAssignerModal.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Modal, Button } from 'react-bootstrap' 3 | 4 | // @flow 5 | 6 | import LabelAssigner from '../components/LabelAssigner' 7 | import type { FeedSource, Project } from '../../types' 8 | 9 | type Props = { 10 | feedSource: FeedSource, 11 | project: Project, 12 | }; 13 | 14 | type State = { 15 | showModal: boolean, 16 | }; 17 | 18 | export default class LabelEditorModal extends React.Component { 19 | state = { 20 | showModal: false 21 | }; 22 | 23 | close = () => { 24 | this.setState({ 25 | showModal: false 26 | }) 27 | }; 28 | 29 | // Used in the ref 30 | open = () => { 31 | this.setState({ 32 | showModal: true 33 | }) 34 | }; 35 | 36 | // Used in the ref 37 | ok = () => { 38 | this.close() 39 | }; 40 | 41 | render () { 42 | const { Body, Header, Title, Footer } = Modal 43 | const { feedSource, project } = this.props 44 | return ( 45 | 46 |
47 | {`Add Labels to ${feedSource.name}`} 48 |
49 | 50 | 51 | 52 | 53 |
54 | 60 |
61 |
62 | ) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/manager/components/LabelEditorModal.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Modal } from 'react-bootstrap' 3 | 4 | // @flow 5 | 6 | import LabelEditor from '../components/LabelEditor' 7 | import type { Label } from '../../types' 8 | 9 | type Props = { 10 | label?: Label, 11 | projectId?: String 12 | } 13 | 14 | type State = { 15 | showModal: boolean 16 | } 17 | 18 | /** 19 | * Renders a LabelEditor within a Modal, including a dynamic title depending 20 | * on if a label is new 21 | */ 22 | export default class LabelEditorModal extends React.Component< 23 | Props, 24 | State 25 | > { 26 | state = { 27 | showModal: false 28 | }; 29 | 30 | close = () => { 31 | this.setState({ 32 | showModal: false 33 | }) 34 | }; 35 | 36 | // Used in the ref 37 | open = () => { 38 | this.setState({ 39 | showModal: true 40 | }) 41 | }; 42 | 43 | // Used in the ref 44 | ok = () => { 45 | this.close() 46 | }; 47 | 48 | render () { 49 | const { Body, Header, Title } = Modal 50 | const label = this.props.label || {} 51 | const { projectId } = this.props 52 | 53 | return ( 54 | 55 |
56 | {label.name ? 'Edit Label' : 'Create New Label'} 57 |
58 | 59 | 60 | 68 | 69 |
70 | ) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /lib/manager/components/ProjectSettings.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React, { Component } from 'react' 4 | import { Row, Col } from 'react-bootstrap' 5 | 6 | import * as projectActions from '../actions/projects' 7 | import type { ManagerUserState } from '../../types/reducers' 8 | import type { Project } from '../../types' 9 | 10 | import ProjectSettingsForm from './ProjectSettingsForm' 11 | 12 | type Props = { 13 | deleteProject: typeof projectActions.deleteProject, 14 | project: Project, 15 | projectEditDisabled: boolean, 16 | updateProject: typeof projectActions.updateProject, 17 | user: ManagerUserState 18 | } 19 | 20 | export default class ProjectSettings extends Component { 21 | _updateProjectSettings = (project: Project, settings: Object) => { 22 | const { updateProject } = this.props 23 | // Update project and re-fetch feeds. 24 | updateProject(project.id, settings, true) 25 | } 26 | 27 | render () { 28 | const { 29 | deleteProject, 30 | project, 31 | projectEditDisabled, 32 | updateProject, 33 | user 34 | } = this.props 35 | return 36 | 37 | 38 | 47 | 48 | 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/manager/components/deployment/DeploymentVersionsTable.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React, {Component} from 'react' 4 | import {Table} from 'react-bootstrap' 5 | 6 | import {getComponentMessages} from '../../../common/util/config' 7 | import DeploymentTableRow from './DeploymentTableRow' 8 | 9 | import type {Deployment, Project, SummarizedFeedVersion} from '../../../types' 10 | 11 | type Props = { 12 | deployment: Deployment, 13 | project: Project, 14 | versions: Array 15 | } 16 | 17 | export default class DeploymentVersionsTable extends Component { 18 | messages = getComponentMessages('DeploymentVersionsTable') 19 | 20 | render () { 21 | const { 22 | deployment, 23 | project, 24 | versions 25 | } = this.props 26 | return ( 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 41 | 42 | 43 | {versions.map((version) => { 44 | return ( 45 | 52 | ) 53 | })} 54 | 55 |
{this.messages('name')}Version{this.messages('loadStatus')}{this.messages('errorCount')}{this.messages('routeCount')}{this.messages('tripCount')}{this.messages('stopTimesCount')}{this.messages('validFrom')}{this.messages('expires')} 40 |
56 | ) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/manager/components/reporter/containers/ActiveDateTimeFilter.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import {connect} from 'react-redux' 4 | 5 | import DateTimeFilter from '../components/DateTimeFilter' 6 | import {updateDateTimeFilter} from '../../../../gtfs/actions/filter' 7 | 8 | import type {FeedVersion} from '../../../../types' 9 | import type {AppState} from '../../../../types/reducers' 10 | 11 | export type Props = { 12 | hideDateTimeField?: boolean, 13 | onChange?: (any) => void, 14 | version: FeedVersion 15 | } 16 | 17 | const mapStateToProps = (state: AppState, ownProps: Props) => { 18 | return { 19 | dateTime: state.gtfs.filter.dateTimeFilter 20 | } 21 | } 22 | 23 | const mapDispatchToProps = { 24 | updateDateTimeFilter 25 | } 26 | 27 | const ActiveDateTimeFilter = connect( 28 | mapStateToProps, 29 | mapDispatchToProps 30 | )(DateTimeFilter) 31 | 32 | export default ActiveDateTimeFilter 33 | -------------------------------------------------------------------------------- /lib/manager/components/reporter/containers/Patterns.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import {connect} from 'react-redux' 4 | 5 | import PatternLayout from '../components/PatternLayout' 6 | import {updatePatternFilter} from '../../../../gtfs/actions/filter' 7 | import { 8 | patternDateTimeFilterChange, 9 | patternRouteFilterChange 10 | } from '../../../../gtfs/actions/patterns' 11 | import {fetchRoutes} from '../../../../gtfs/actions/routes' 12 | import {timetablePatternFilterChange} from '../../../../gtfs/actions/timetables' 13 | import {getPatternData} from '../../../selectors' 14 | 15 | import type {FeedVersion} from '../../../../types' 16 | import type {AppState} from '../../../../types/reducers' 17 | 18 | export type Props = { 19 | selectTab: string => void, 20 | version: FeedVersion 21 | } 22 | 23 | const mapStateToProps = (state: AppState, ownProps: Props) => { 24 | return { 25 | fetchStatus: state.gtfs.patterns.fetchStatus, 26 | namespace: ownProps.version.namespace, 27 | routes: state.gtfs.routes.allRoutes, 28 | routeFilter: state.gtfs.filter.routeFilter, 29 | patternData: getPatternData(state) 30 | } 31 | } 32 | 33 | const mapDispatchToProps = { 34 | fetchRoutes, 35 | patternDateTimeFilterChange, 36 | patternRouteFilterChange, 37 | timetablePatternFilterChange, 38 | updatePatternFilter 39 | } 40 | 41 | const Patterns = connect( 42 | mapStateToProps, 43 | mapDispatchToProps 44 | )(PatternLayout) 45 | 46 | export default Patterns 47 | -------------------------------------------------------------------------------- /lib/manager/components/reporter/containers/Routes.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import {connect} from 'react-redux' 4 | 5 | import RouteLayout from '../components/RouteLayout' 6 | import {fetchRoutes, fetchRouteDetails, routeOffsetChange} from '../../../../gtfs/actions/routes' 7 | import {patternRouteFilterChange} from '../../../../gtfs/actions/patterns' 8 | import {getRouteData} from '../../../selectors' 9 | 10 | import type {FeedVersion} from '../../../../types' 11 | import type {AppState} from '../../../../types/reducers' 12 | 13 | export type Props = { 14 | selectTab: string => void, 15 | version: FeedVersion 16 | } 17 | 18 | const mapStateToProps = (state: AppState, ownProps: Props) => { 19 | const {namespace} = ownProps.version 20 | const {gtfs} = state 21 | const {filter, routes} = gtfs 22 | const {routeOffset} = filter 23 | const {data, fetchStatus} = routes.routeDetails 24 | 25 | return { 26 | fetchStatus, 27 | namespace, 28 | allRoutes: state.gtfs.routes.allRoutes, 29 | numRoutes: data ? data.numRoutes : 0, 30 | routeData: getRouteData(state), 31 | routeOffset 32 | } 33 | } 34 | 35 | const mapDispatchToProps = { 36 | fetchRouteDetails, 37 | fetchRoutes, 38 | patternRouteFilterChange, 39 | routeOffsetChange 40 | } 41 | 42 | const Routes = connect( 43 | mapStateToProps, 44 | mapDispatchToProps 45 | )(RouteLayout) 46 | 47 | export default Routes 48 | -------------------------------------------------------------------------------- /lib/manager/components/reporter/containers/Stops.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import {connect} from 'react-redux' 4 | 5 | import StopLayout from '../components/StopLayout' 6 | import {updatePatternFilter} from '../../../../gtfs/actions/filter' 7 | import { 8 | patternDateTimeFilterChange, 9 | patternRouteFilterChange 10 | } from '../../../../gtfs/actions/patterns' 11 | import {fetchRoutes} from '../../../../gtfs/actions/routes' 12 | import {getFilteredStops} from '../../../selectors' 13 | 14 | import type {FeedVersion} from '../../../../types' 15 | import type {AppState} from '../../../../types/reducers' 16 | 17 | export type Props = { 18 | selectTab: (string) => void, 19 | tableOptions: any, 20 | version: FeedVersion 21 | } 22 | 23 | const mapStateToProps = (state: AppState, ownProps: Props) => { 24 | const {gtfs} = state 25 | const {filter, patterns, routes} = gtfs 26 | const {patternFilter, routeFilter} = filter 27 | 28 | return { 29 | patternFilter, 30 | patterns: patterns.data.patterns, 31 | routeFilter, 32 | routes: routes.allRoutes, 33 | stops: getFilteredStops(state) 34 | } 35 | } 36 | 37 | const mapDispatchToProps = { 38 | fetchRoutes, 39 | patternDateTimeFilterChange, 40 | patternRouteFilterChange, 41 | updatePatternFilter 42 | } 43 | 44 | export default connect( 45 | mapStateToProps, 46 | mapDispatchToProps 47 | )(StopLayout) 48 | -------------------------------------------------------------------------------- /lib/manager/components/reporter/containers/Timetables.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import {connect} from 'react-redux' 4 | 5 | import TimetableLayout from '../components/TimetableLayout' 6 | import {fetchRoutes} from '../../../../gtfs/actions/routes' 7 | import { 8 | fetchTimetablesWithFilters, 9 | timetableDateTimeFilterChange, 10 | timetablePatternFilterChange, 11 | timetableRouteFilterChange, 12 | timetableShowArrivalToggle, 13 | timetableTimepointToggle 14 | } from '../../../../gtfs/actions/timetables' 15 | import {getTimetableData} from '../../../selectors' 16 | 17 | import type {FeedVersion} from '../../../../types' 18 | import type {AppState} from '../../../../types/reducers' 19 | 20 | export type Props = { 21 | selectTab: string => void, 22 | tableOptions: any, 23 | version: FeedVersion 24 | } 25 | 26 | const mapStateToProps = (state: AppState, ownProps: Props) => { 27 | const {gtfs} = state 28 | const { 29 | filter, 30 | patterns, 31 | routes, 32 | timetables 33 | } = gtfs 34 | 35 | return { 36 | filter, 37 | patterns, 38 | routes: routes.allRoutes, 39 | timetables, 40 | timetableTableData: getTimetableData(state) 41 | } 42 | } 43 | 44 | const mapDispatchToProps = { 45 | fetchRoutes, 46 | fetchTimetablesWithFilters, 47 | timetableDateTimeFilterChange, 48 | timetablePatternFilterChange, 49 | timetableRouteFilterChange, 50 | timetableShowArrivalToggle, 51 | timetableTimepointToggle 52 | } 53 | 54 | export default connect(mapStateToProps, mapDispatchToProps)(TimetableLayout) 55 | -------------------------------------------------------------------------------- /lib/manager/components/transform/PreserveCustomFields.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { getComponentMessages } from '../../../common/util/config' 4 | 5 | import ReplaceFileFromString from './ReplaceFileFromString' 6 | import CustomCSVForm from './CustomCSVForm' 7 | 8 | export default class PreserveCustomFields extends ReplaceFileFromString { 9 | // Messages are for the child CSV Form component but since messages are shared across transformation types, 10 | // the messages are being grouped under that component. 11 | messages = getComponentMessages('PreserveCustomFields') 12 | render () { 13 | const {transformation} = this.props 14 | const {csvData} = this.state 15 | const inputIsSame = csvData === transformation.csvData 16 | 17 | return ( 18 | 26 | ) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/manager/components/transform/TransformationsIndicatorBadge.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import Icon from '@conveyal/woonerf/components/icon' 4 | import React, {Component} from 'react' 5 | import {OverlayTrigger, Tooltip} from 'react-bootstrap' 6 | 7 | import type { FeedVersion } from '../../../types' 8 | 9 | type Props = { 10 | version: FeedVersion 11 | } 12 | 13 | type State = { 14 | expanded: boolean 15 | } 16 | 17 | /** 18 | * Renders a badge with tooltip showing a summary of the transformations applied 19 | * to the feed version during processing by the server backend. 20 | */ 21 | export default class TransformationsIndicatorBadge extends Component { 22 | state = {expanded: false} 23 | 24 | _renderTooltipContent = () => { 25 | const {version} = this.props 26 | const {feedTransformResult} = version 27 | if (!feedTransformResult) return null 28 | const transformationCount = feedTransformResult.tableTransformResults.length 29 | 30 | return ( 31 |
32 | Feed has {transformationCount} transformation(s): 33 |
    34 | {feedTransformResult.tableTransformResults.map((item, i) => { 35 | return ( 36 |
  • 37 | {item.tableName}{' '} 38 | {item.transformType.toLowerCase().replace('_', ' ')} 39 |
  • 40 | ) 41 | })} 42 |
43 |
44 | ) 45 | } 46 | 47 | render () { 48 | const { version } = this.props 49 | if (!version.feedTransformResult) return null 50 | return ( 51 | 54 | {this._renderTooltipContent()} 55 | 56 | }> 57 | 60 | 61 | ) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/manager/components/version/DeltaStat.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from 'react' 4 | import {Label as BsLabel, OverlayTrigger, Tooltip} from 'react-bootstrap' 5 | 6 | import {formatDelta} from '../../util/version' 7 | 8 | /** 9 | * Renders a delta statistic as a label with tooltip when comparing feed 10 | * versions. 11 | */ 12 | const DeltaStat = ( 13 | { 14 | comparedVersionIndex, 15 | diff, 16 | inverse, 17 | style 18 | }: { 19 | comparedVersionIndex: number, 20 | diff: number, 21 | inverse?: boolean, 22 | style: any 23 | }) => { 24 | // Construct the tooltip label and style based on positive/negative diff. 25 | let bsStyle = 'default' 26 | let comparePhrase = 'No change' 27 | let conjunction = 'from' 28 | if (diff > 0) { 29 | comparePhrase = `${Math.abs(diff)} more` 30 | conjunction = 'than' 31 | if (inverse) bsStyle = 'danger' 32 | else bsStyle = 'success' 33 | } else if (diff < 0) { 34 | comparePhrase = `${Math.abs(diff)} fewer` 35 | conjunction = 'than' 36 | if (inverse) bsStyle = 'success' 37 | else bsStyle = 'danger' 38 | } 39 | // Render the label 40 | return ( 41 | 45 | {comparePhrase} {conjunction} version {comparedVersionIndex} 46 | 47 | } 48 | > 49 | 53 | {formatDelta(diff)} 54 | 55 | 56 | ) 57 | } 58 | 59 | export default DeltaStat 60 | -------------------------------------------------------------------------------- /lib/manager/components/version/VersionDateLabel.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React, {Component} from 'react' 4 | import {Label as BsLabel} from 'react-bootstrap' 5 | import moment from 'moment' 6 | 7 | import type {FeedVersionSummary} from '../../../types' 8 | 9 | type Props = { 10 | version: FeedVersionSummary 11 | } 12 | 13 | export default class VersionDateLabel extends Component { 14 | render () { 15 | const {version} = this.props 16 | const {validationSummary: summary} = version 17 | if (!summary) return null 18 | const now = +moment().startOf('day') 19 | const start = +moment(summary.startDate) 20 | const end = +moment(summary.endDate) 21 | const future = start > now 22 | const expired = end < now 23 | const bsStyle = future ? 'info' : expired ? 'danger' : 'success' 24 | const text = future ? 'future' : expired ? 'expired' : 'active' 25 | return ( 26 | 28 | {text} 29 | 30 | ) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/manager/components/version/VersionRetrievalBadge.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import Icon from '@conveyal/woonerf/components/icon' 4 | import React from 'react' 5 | 6 | import toSentenceCase from '../../../common/util/text' 7 | import type { FeedVersionSummary, RetrievalMethod } from '../../../types' 8 | 9 | type Props = { 10 | retrievalMethod?: RetrievalMethod, 11 | version?: FeedVersionSummary 12 | } 13 | 14 | const iconForRetrievalMethod = (retrievalMethod: RetrievalMethod | 'UNKNOWN') => { 15 | switch (retrievalMethod) { 16 | case 'SERVICE_PERIOD_MERGE': 17 | return 'code-fork' 18 | case 'REGIONAL_MERGE': 19 | return 'globe' 20 | case 'PRODUCED_IN_HOUSE': 21 | case 'PRODUCED_IN_HOUSE_GTFS_PLUS': 22 | return 'pencil' 23 | case 'MANUALLY_UPLOADED': 24 | return 'upload' 25 | case 'FETCHED_AUTOMATICALLY': 26 | return 'cloud-download' 27 | case 'VERSION_CLONE': 28 | return 'clone' 29 | default: 30 | return 'file-archive-o' 31 | } 32 | } 33 | 34 | export default function VersionRetrievalBadge (props: Props) { 35 | const { retrievalMethod, version } = props 36 | const method = retrievalMethod || (version && version.retrievalMethod) || 'UNKNOWN' 37 | return ( 38 | 42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /lib/manager/containers/ActiveDeploymentViewer.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import {connect} from 'react-redux' 4 | 5 | import { 6 | addFeedVersion, 7 | deployToTarget, 8 | downloadBuildArtifact, 9 | downloadDeployment, 10 | downloadDeploymentShapes, 11 | fetchDeployment, 12 | incrementAllVersionsToLatest, 13 | terminateEC2InstanceForDeployment, 14 | updateDeployment 15 | } from '../actions/deployments' 16 | import DeploymentViewer from '../components/deployment/DeploymentViewer' 17 | import type {Deployment, Feed, FeedSourceSummary, Project} from '../../types' 18 | import type {AppState} from '../../types/reducers' 19 | 20 | export type Props = { 21 | deployment: Deployment, 22 | feedSources: Array, 23 | project: Project 24 | } 25 | 26 | const mapStateToProps = (state: AppState, ownProps: Props) => { 27 | const user = state.user 28 | const deployJobs = state.status.jobMonitor.jobs 29 | .filter(job => job.deploymentId === ownProps.deployment.id) 30 | return { 31 | deployJobs, 32 | user 33 | } 34 | } 35 | 36 | const mapDispatchToProps = { 37 | addFeedVersion, 38 | deployToTarget, 39 | downloadBuildArtifact, 40 | downloadDeployment, 41 | downloadDeploymentShapes, 42 | fetchDeployment, 43 | incrementAllVersionsToLatest, 44 | terminateEC2InstanceForDeployment, 45 | updateDeployment 46 | } 47 | 48 | const ActiveDeploymentViewer = connect( 49 | mapStateToProps, 50 | mapDispatchToProps 51 | )(DeploymentViewer) 52 | 53 | export default ActiveDeploymentViewer 54 | -------------------------------------------------------------------------------- /lib/manager/containers/ActiveProjectViewer.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import {connect} from 'react-redux' 4 | 5 | import {fetchProjectDeployments} from '../actions/deployments' 6 | import {createFeedSource} from '../actions/feeds' 7 | import { 8 | deleteProject, 9 | deployPublic, 10 | onProjectViewerMount, 11 | updateProject 12 | } from '../actions/projects' 13 | import ProjectViewer from '../components/ProjectViewer' 14 | 15 | import type {AppState, RouterProps} from '../../types/reducers' 16 | 17 | export type Props = RouterProps 18 | 19 | const mapStateToProps = (state: AppState, ownProps: Props) => { 20 | const {user} = state 21 | const {all, isFetching} = state.projects 22 | const { 23 | projectId, 24 | subpage: activeComponent, 25 | subsubpage: activeSubComponent 26 | } = ownProps.routeParams 27 | const project = all ? all.find(p => p.id === projectId) : null 28 | return { 29 | activeComponent, 30 | activeSubComponent, 31 | isFetching, 32 | project, 33 | projectId, 34 | user 35 | } 36 | } 37 | 38 | const mapDispatchToProps = { 39 | createFeedSource, 40 | deleteProject, 41 | deployPublic, 42 | fetchProjectDeployments, 43 | onProjectViewerMount, 44 | updateProject 45 | } 46 | 47 | const ActiveProjectViewer = connect(mapStateToProps, mapDispatchToProps)( 48 | ProjectViewer 49 | ) 50 | 51 | export default ActiveProjectViewer 52 | -------------------------------------------------------------------------------- /lib/manager/containers/ActiveProjectsList.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import {connect} from 'react-redux' 4 | 5 | import {fetchProjects, updateProject} from '../actions/projects' 6 | import {setVisibilitySearchText} from '../actions/visibilityFilter' 7 | import ProjectsList from '../components/ProjectsList' 8 | import {getProjects} from '../selectors' 9 | 10 | import type {AppState, RouterProps} from '../../types/reducers' 11 | 12 | export type Props = RouterProps 13 | 14 | const mapStateToProps = (state: AppState, ownProps: Props) => { 15 | return { 16 | projects: getProjects(state), 17 | user: state.user, 18 | visibilitySearchText: state.projects.filter.searchText 19 | } 20 | } 21 | 22 | const mapDispatchToProps = { 23 | fetchProjects, 24 | setVisibilitySearchText, 25 | updateProject 26 | } 27 | 28 | const ActiveProjectsList = connect( 29 | mapStateToProps, 30 | mapDispatchToProps 31 | )(ProjectsList) 32 | 33 | export default ActiveProjectsList 34 | -------------------------------------------------------------------------------- /lib/manager/containers/ActiveUserHomePage.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import {connect} from 'react-redux' 4 | import { withAuth0 } from '@auth0/auth0-react' 5 | 6 | import {logout, onUserHomeMount} from '../actions/user' 7 | import {fetchProjectFeeds} from '../actions/feeds' 8 | import {setVisibilitySearchText, setVisibilityFilter} from '../actions/visibilityFilter' 9 | import UserHomePage from '../components/UserHomePage' 10 | import type {AppState, RouterProps} from '../../types/reducers' 11 | 12 | export type Props = RouterProps 13 | 14 | const mapStateToProps = (state: AppState, ownProps: Props) => { 15 | const {projects, user} = state 16 | return { 17 | user, 18 | projects: projects.all 19 | ? projects.all.filter(p => p.isCreating || 20 | (user.permissions && user.permissions.isApplicationAdmin()) || 21 | (user.permissions && user.permissions.hasProject(p.id, p.organizationId))) 22 | : [], 23 | project: ownProps.routeParams.projectId && projects.all 24 | ? projects.all.find(p => p.id === ownProps.routeParams.projectId) 25 | : null, 26 | projectId: ownProps.routeParams.projectId, 27 | visibilityFilter: projects.filter 28 | } 29 | } 30 | 31 | const mapDispatchToProps = { 32 | fetchProjectFeeds, 33 | logout, 34 | onUserHomeMount, 35 | setVisibilityFilter, 36 | setVisibilitySearchText 37 | } 38 | 39 | const ActiveUserHomePage = withAuth0(connect( 40 | mapStateToProps, 41 | mapDispatchToProps 42 | )(UserHomePage)) 43 | 44 | export default ActiveUserHomePage 45 | -------------------------------------------------------------------------------- /lib/manager/containers/CreateProject.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { connect } from 'react-redux' 4 | 5 | import { createProject } from '../actions/projects' 6 | import CreateProject from '../components/CreateProject' 7 | 8 | const mapStateToProps = (state, ownProps) => { 9 | return { 10 | user: state.user 11 | } 12 | } 13 | 14 | const mapDispatchToProps = { 15 | createProject 16 | } 17 | 18 | export default connect( 19 | mapStateToProps, 20 | mapDispatchToProps 21 | )(CreateProject) 22 | -------------------------------------------------------------------------------- /lib/manager/containers/DeploymentsPanel.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import {connect} from 'react-redux' 4 | 5 | import {updateProject} from '../actions/projects' 6 | import { 7 | createDeployment, 8 | saveDeployment, 9 | deleteDeployment, 10 | updateDeployment 11 | } from '../actions/deployments' 12 | import DeploymentsPanel from '../components/deployment/DeploymentsPanel' 13 | 14 | import type {Project} from '../../types' 15 | import type {AppState} from '../../types/reducers' 16 | 17 | export type Props = { 18 | activeSubComponent: ?string, 19 | expanded: boolean, 20 | project: Project 21 | } 22 | 23 | const mapStateToProps = (state: AppState, ownProps: Props) => ({}) 24 | 25 | const mapDispatchToProps = { 26 | createDeployment, 27 | deleteDeployment, 28 | saveDeployment, 29 | updateDeployment, 30 | updateProject 31 | } 32 | 33 | export default connect(mapStateToProps, mapDispatchToProps)(DeploymentsPanel) 34 | -------------------------------------------------------------------------------- /lib/manager/containers/FeedSourceTable.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import {connect} from 'react-redux' 4 | 5 | import FeedSourceTable from '../components/FeedSourceTable' 6 | import {getFilteredFeeds} from '../util' 7 | import type {Project} from '../../types' 8 | import type {AppState} from '../../types/reducers' 9 | 10 | export type Props = { 11 | onNewFeedSourceClick: () => void, 12 | project: Project 13 | } 14 | 15 | const mapStateToProps = (state: AppState, ownProps: Props) => { 16 | const {user} = state 17 | const {filter, isFetching, sort} = state.projects 18 | const {project} = ownProps 19 | const isNotAdmin = !user.permissions || 20 | !user.permissions.isProjectAdmin(project.id, project.organizationId) 21 | const feedSources = project.feedSources ? project.feedSources : [] 22 | 23 | return { 24 | comparisonColumn: filter.feedSourceTableComparisonColumn, 25 | feedSources, 26 | filteredFeedSources: getFilteredFeeds( 27 | feedSources, 28 | filter, 29 | project, 30 | sort 31 | ), 32 | isFetching, 33 | isNotAdmin, 34 | sort, 35 | user 36 | } 37 | } 38 | 39 | const mapDispatchToProps = {} 40 | 41 | export default connect(mapStateToProps, mapDispatchToProps)(FeedSourceTable) 42 | -------------------------------------------------------------------------------- /lib/manager/containers/FeedSourceTableRow.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import {connect} from 'react-redux' 4 | 5 | import {createDeploymentFromFeedSource} from '../actions/deployments' 6 | import { 7 | createFeedSource, 8 | deleteFeedSource, 9 | runFetchFeed, 10 | updateFeedSource 11 | } from '../actions/feeds' 12 | import {uploadFeed} from '../actions/versions' 13 | import FeedSourceTableRow from '../components/FeedSourceTableRow' 14 | import {getVersionValidationSummaryByFilterStrategy} from '../util/version' 15 | 16 | import type {Feed, Project} from '../../types' 17 | import type {AppState} from '../../types/reducers' 18 | 19 | export type Props = { 20 | feedSource: Feed, 21 | project: Project 22 | } 23 | 24 | const mapStateToProps = (state: AppState, ownProps: Props) => { 25 | const {projects, user} = state 26 | const {feedSource, project} = ownProps 27 | const comparisonColumn = projects.filter.feedSourceTableComparisonColumn 28 | 29 | return { 30 | comparisonColumn, 31 | comparisonValidationSummary: getVersionValidationSummaryByFilterStrategy( 32 | project, 33 | feedSource, 34 | comparisonColumn 35 | ), 36 | user 37 | } 38 | } 39 | 40 | const mapDispatchToProps = { 41 | createDeploymentFromFeedSource, 42 | createFeedSource, 43 | deleteFeedSource, 44 | runFetchFeed, 45 | updateFeedSource, 46 | uploadFeed 47 | } 48 | 49 | export default connect(mapStateToProps, mapDispatchToProps)(FeedSourceTableRow) 50 | -------------------------------------------------------------------------------- /lib/manager/containers/__tests__/ActiveProjectViewer.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import ActiveProjectViewer from '../ActiveProjectViewer' 4 | import mockData from '../../../../__tests__/test-utils/mock-data' 5 | 6 | const {store} = mockData 7 | 8 | describe('lib > manager > ActiveProjectViewer', () => { 9 | it('should render with newly created project', () => { 10 | const mockState = store.getMockStateWithProject() 11 | expect( 12 | store.mockWithProvider( 13 | ActiveProjectViewer, 14 | mockState.routing, 15 | mockState 16 | ).snapshot() 17 | ).toMatchSnapshot() 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /lib/manager/containers/__tests__/DeploymentsPanel.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import DeploymentsPanel from '../DeploymentsPanel' 4 | import { 5 | restoreDateNowBehavior, 6 | setDefaultTestTime 7 | } from '../../../../__tests__/test-utils' 8 | import mockData from '../../../../__tests__/test-utils/mock-data' 9 | 10 | const {manager, store} = mockData 11 | 12 | describe('lib > manager > DeploymentsPanel', () => { 13 | afterEach(restoreDateNowBehavior) 14 | 15 | it('should render with the list of deployments of a project with deployments', () => { 16 | setDefaultTestTime() 17 | const mockState = store.getMockStateWithProjectWithFeedsAndDeployment() 18 | const mockProject = mockState.projects.all[1] 19 | 20 | // add another deployment without any feed versions to the project deployments 21 | mockProject.deployments.push(manager.makeMockDeployment(mockProject)) 22 | mockProject.pinnedDeploymentId = mockProject.deployments[0].id 23 | 24 | expect( 25 | store.mockWithProvider( 26 | DeploymentsPanel, 27 | { 28 | activeSubComponent: null, 29 | expanded: true, 30 | project: mockProject 31 | }, 32 | mockState 33 | ).snapshot() 34 | ).toMatchSnapshot() 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /lib/manager/containers/__tests__/FeedSourceTable.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import FeedSourceTable from '../FeedSourceTable' 4 | import { 5 | restoreDateNowBehavior, 6 | setDefaultTestTime 7 | } from '../../../../__tests__/test-utils' 8 | import mockData from '../../../../__tests__/test-utils/mock-data' 9 | 10 | const {manager, store} = mockData 11 | 12 | describe('lib > manager > FeedSourceTable', () => { 13 | afterEach(restoreDateNowBehavior) 14 | 15 | it('should render with a project with feeds and a deployment', () => { 16 | setDefaultTestTime() 17 | const mockState = store.getMockStateWithProjectWithFeedsAndDeployment() 18 | 19 | expect( 20 | store.mockWithProvider( 21 | FeedSourceTable, 22 | { 23 | onNewFeedSourceClick: () => null, 24 | project: manager.mockProjectWithDeployment 25 | }, 26 | mockState 27 | ).snapshot() 28 | ).toMatchSnapshot() 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /lib/manager/reducers/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import languages from './languages' 4 | import projects from './projects' 5 | import status from './status' 6 | import ui from './ui' 7 | import user from './user' 8 | 9 | export default { 10 | languages, 11 | projects, 12 | status, 13 | ui, 14 | user 15 | } 16 | -------------------------------------------------------------------------------- /lib/manager/reducers/languages.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import update from 'react-addons-update' 4 | import { getConfigProperty } from '../../common/util/config' 5 | 6 | import type {Action} from '../../types/actions' 7 | import type {DataToolsConfig} from '../../types' 8 | import type {LanguagesState} from '../../types/reducers' 9 | 10 | export const defaultState = { 11 | all: getConfigProperty('messages.all'), 12 | // set active default to english 13 | active: getConfigProperty('messages.active') 14 | } 15 | 16 | const languages = (state: LanguagesState = defaultState, action: Action): LanguagesState => { 17 | let languageIndex 18 | switch (action.type) { 19 | case 'SET_ACTIVE_LANGUAGE': 20 | const language = action.payload 21 | languageIndex = state.all.findIndex(l => l.id === language) 22 | const CONFIG: DataToolsConfig = window.DT_CONFIG 23 | CONFIG.messages.active = state.all[languageIndex] 24 | window.localStorage.setItem('lang', language) 25 | return update(state, {active: { $set: state.all[languageIndex] }}) 26 | default: 27 | return state 28 | } 29 | } 30 | 31 | export default languages 32 | -------------------------------------------------------------------------------- /lib/manager/reducers/ui.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import update from 'react-addons-update' 4 | import { getUserMetadataProperty } from '../../common/util/user' 5 | 6 | import type {UiState} from '../../types/reducers' 7 | 8 | export const defaultState = { 9 | sidebarExpanded: true, 10 | hideTutorial: false 11 | } 12 | 13 | const ui = (state: UiState = defaultState, action: any): UiState => { 14 | switch (action.type) { 15 | case 'USER_LOGGED_IN': 16 | const hideTutorial = getUserMetadataProperty(action.profile, 'hideTutorial') 17 | const sidebarExpanded = getUserMetadataProperty(action.profile, 'sidebarExpanded') 18 | return update(state, { 19 | sidebarExpanded: { $set: sidebarExpanded }, 20 | hideTutorial: { $set: hideTutorial } 21 | }) 22 | case 'SETTING_TUTORIAL_VISIBILITY': 23 | return update(state, { hideTutorial: { $set: action.value } }) 24 | case 'SETTING_SIDEBAR_EXPANDED': 25 | return update(state, { sidebarExpanded: { $set: action.payload } }) 26 | default: 27 | return state 28 | } 29 | } 30 | 31 | export default ui 32 | -------------------------------------------------------------------------------- /lib/manager/reducers/user.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import UserPermissions from '../../common/user/UserPermissions' 4 | import UserSubscriptions from '../../common/user/UserSubscriptions' 5 | import type {Action} from '../../types/actions' 6 | import type {ManagerUserState} from '../../types/reducers' 7 | 8 | export const defaultState = { 9 | isCheckingLogin: true, 10 | token: null, 11 | profile: null, 12 | permissions: null, 13 | recentActivity: null, 14 | subscriptions: null 15 | } 16 | 17 | const user = (state: ManagerUserState = defaultState, action: Action): ManagerUserState => { 18 | switch (action.type) { 19 | case 'USER_LOGGED_IN': 20 | return { 21 | ...state, 22 | isCheckingLogin: false, 23 | token: action.payload.token, 24 | profile: action.payload.profile, 25 | permissions: new UserPermissions(action.payload.profile.app_metadata.datatools), 26 | subscriptions: new UserSubscriptions(action.payload.profile.app_metadata.datatools) 27 | } 28 | case 'USER_PROFILE_UPDATED': 29 | return { 30 | ...state, 31 | profile: action.payload, 32 | permissions: new UserPermissions(action.payload.app_metadata.datatools), 33 | subscriptions: new UserSubscriptions(action.payload.app_metadata.datatools) 34 | } 35 | case 'USER_LOGGED_OUT': 36 | return { 37 | ...state, 38 | isCheckingLogin: false, 39 | token: null, 40 | profile: null, 41 | permissions: null, 42 | subscriptions: null 43 | } 44 | case 'CREATED_PUBLIC_USER': 45 | return { 46 | ...state, 47 | profile: action.payload.profile, 48 | permissions: new UserPermissions(action.payload.profile.app_metadata.datatools), 49 | subscriptions: new UserSubscriptions(action.payload.profile.app_metadata.datatools) 50 | } 51 | case 'RECEIVE_USER_RECENT_ACTIVITY': 52 | return {...state, recentActivity: action.payload} 53 | default: 54 | return state 55 | } 56 | } 57 | 58 | export default user 59 | -------------------------------------------------------------------------------- /lib/manager/util/__tests__/__snapshots__/transform.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`manager > util > transform > getTransformationName > should return fully-replaced label for ReplaceFileFromStringTranformation 1`] = `"Replace feed_info table from below text"`; 4 | 5 | exports[`manager > util > transform > getTransformationName > should return half-replaced label for ReplaceFileFromStringTranformation 1`] = `"Replace [choose table] from below text"`; 6 | 7 | exports[`manager > util > transform > getTransformationName > should return name for ReplaceFileFromStringTranformation 1`] = `"Replace file from string transformation"`; 8 | 9 | exports[`manager > util > transform > getTransformationName > should return unreplaced label for ReplaceFileFromStringTranformation 1`] = `"Replace [choose table] from [choose file]"`; 10 | -------------------------------------------------------------------------------- /lib/manager/util/__tests__/transform.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import clone from 'lodash/cloneDeep' 3 | 4 | import {getTransformationName} from '../transform' 5 | import {FEED_TRANSFORMATION_TYPES} from '../../../common/constants' 6 | 7 | import type {ReplaceFileFromStringTransformation} from '../../../types' 8 | 9 | const {REPLACE_FILE_FROM_STRING} = FEED_TRANSFORMATION_TYPES 10 | 11 | const replaceFileTransformation: ReplaceFileFromStringTransformation = { 12 | '@type': REPLACE_FILE_FROM_STRING, 13 | active: true, 14 | csvData: null, 15 | sourceVersionId: 'abcdefg-12345.zip', 16 | table: null, 17 | typeName: REPLACE_FILE_FROM_STRING 18 | } 19 | 20 | const halfCompletedTransformation = clone(replaceFileTransformation) 21 | halfCompletedTransformation.csvData = 'feed_id\n1' 22 | 23 | const fullyCompletedTransformation = clone(halfCompletedTransformation) 24 | fullyCompletedTransformation.table = 'feed_info' 25 | 26 | describe('manager > util > transform >', () => { 27 | describe('getTransformationName >', () => { 28 | it('should return name for ReplaceFileFromStringTranformation', () => { 29 | expect(getTransformationName(REPLACE_FILE_FROM_STRING)).toMatchSnapshot() 30 | }) 31 | 32 | it('should return unreplaced label for ReplaceFileFromStringTranformation', () => { 33 | expect( 34 | getTransformationName( 35 | REPLACE_FILE_FROM_STRING, 36 | replaceFileTransformation 37 | ) 38 | ).toMatchSnapshot() 39 | }) 40 | 41 | it('should return half-replaced label for ReplaceFileFromStringTranformation', () => { 42 | expect( 43 | getTransformationName( 44 | REPLACE_FILE_FROM_STRING, 45 | halfCompletedTransformation 46 | ) 47 | ).toMatchSnapshot() 48 | }) 49 | 50 | it('should return fully-replaced label for ReplaceFileFromStringTranformation', () => { 51 | expect( 52 | getTransformationName( 53 | REPLACE_FILE_FROM_STRING, 54 | fullyCompletedTransformation 55 | ) 56 | ).toMatchSnapshot() 57 | }) 58 | }) 59 | }) 60 | -------------------------------------------------------------------------------- /lib/manager/util/enums/transform.js: -------------------------------------------------------------------------------- 1 | // CSV validation errors for transformations. 2 | // Values are keys to YAML error messages. 3 | const CSV_VALIDATION_ERRORS = { 4 | CSV_MUST_HAVE_NAME: 'csvMissingName', 5 | CSV_NAME_CONTAINS_TXT: 'csvNameContainsTxt', 6 | TABLE_MUST_BE_DEFINED: 'undefinedTable', 7 | UNDEFINED_CSV_DATA: 'undefinedCSVData' 8 | } 9 | 10 | export default CSV_VALIDATION_ERRORS 11 | -------------------------------------------------------------------------------- /lib/manager/util/validation.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | /** 4 | * Checks if a filename is valid. 5 | * A valid filename does not contain <>:"/\|?* characters and does not end with .zip (case-insensitive). 6 | * An empty or null/undefined filename is also considered valid. 7 | */ 8 | export function isValidFilename (filename: ?string): boolean { 9 | // Ensure only valid characters (no ., <>,:"/\\|?*, or space) are used 10 | return !filename || /^[^<>:"/\\|?* .]*$/.test(filename) 11 | } 12 | -------------------------------------------------------------------------------- /lib/public/components/PublicPage.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React, {Component, type Node} from 'react' 4 | 5 | import CurrentStatusMessage from '../../common/containers/CurrentStatusMessage' 6 | import ConfirmModal from '../../common/components/ConfirmModal' 7 | import SelectFileModal from '../../common/components/SelectFileModal' 8 | import Title from '../../common/components/Title' 9 | import ActivePublicHeader from '../containers/ActivePublicHeader' 10 | import { getConfigProperty } from '../../common/util/config' 11 | 12 | type Props = { 13 | children?: Node 14 | } 15 | 16 | export default class PublicPage extends Component { 17 | showConfirmModal (props: any) { 18 | this.refs.confirmModal.open(props) 19 | } 20 | 21 | showSelectFileModal (props: any) { 22 | this.refs.selectFileModal.open(props) 23 | } 24 | 25 | render () { 26 | const appTitle = getConfigProperty('application.title') || 'Data Tools' 27 | return ( 28 |
29 | {appTitle} 30 | 31 | {this.props.children} 32 | 33 | 34 | 35 |
36 | ) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/public/containers/ActiveLicenseTerms.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { withAuth0 } from '@auth0/auth0-react' 4 | import { connect } from 'react-redux' 5 | import qs from 'qs' 6 | 7 | import { getAccountTypes, getSettingsFromProfile } from '../../common/util/user' 8 | import LicenseTerms from '../components/LicenseTerms' 9 | import * as userActions from '../../manager/actions/user' 10 | import type { AppState } from '../../types/reducers' 11 | 12 | const mapStateToProps = (state: AppState) => { 13 | const { user } = state 14 | const userSettings = getSettingsFromProfile(user.profile) 15 | const accountType = userSettings && userSettings.account_type && 16 | getAccountTypes(state)[userSettings.account_type] 17 | const search = window.location.search 18 | return { 19 | // Exclude the `?` at the beginning of the search term if it exists. 20 | // $FlowFixMe flow is missing definitions for qs.parse. 21 | returnTo: search.length ? qs.parse(search.substring(1)).returnTo : null, 22 | termsUrl: accountType && accountType.terms_url, 23 | user 24 | } 25 | } 26 | 27 | const mapDispatchToProps = { 28 | acceptAccountTerms: userActions.acceptAccountTerms, 29 | logout: userActions.logout 30 | } 31 | 32 | export default withAuth0(connect( 33 | mapStateToProps, 34 | mapDispatchToProps 35 | )(LicenseTerms)) 36 | -------------------------------------------------------------------------------- /lib/public/containers/ActivePublicHeader.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { withAuth0 } from '@auth0/auth0-react' 4 | import { connect } from 'react-redux' 5 | 6 | import PublicHeader from '../components/PublicHeader' 7 | import * as userActions from '../../manager/actions/user' 8 | import { getConfigProperty } from '../../common/util/config' 9 | import type { AppState } from '../../types/reducers' 10 | 11 | export type Props = {} 12 | 13 | const mapStateToProps = (state: AppState, ownProps: Props) => { 14 | return { 15 | title: getConfigProperty('application.title'), 16 | username: state.user.profile ? state.user.profile.email : null, 17 | userPicture: state.user.profile ? state.user.profile.picture : null 18 | } 19 | } 20 | 21 | const mapDispatchToProps = { 22 | logout: userActions.logout 23 | } 24 | 25 | export default withAuth0(connect( 26 | mapStateToProps, 27 | mapDispatchToProps 28 | )(PublicHeader) 29 | ) 30 | -------------------------------------------------------------------------------- /lib/public/containers/ActivePublicLandingPage.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { connect } from 'react-redux' 4 | 5 | import PublicLandingPage from '../components/PublicLandingPage' 6 | import { setVisibilitySearchText } from '../../manager/actions/visibilityFilter' 7 | import { fetchProjects } from '../../manager/actions/projects' 8 | 9 | import type { AppState, RouterProps } from '../../types/reducers' 10 | 11 | export type Props = RouterProps 12 | 13 | const mapStateToProps = (state: AppState, ownProps: Props) => { 14 | return { 15 | visibilitySearchText: state.projects.filter.searchText, 16 | projects: state.projects.all, 17 | user: state.user 18 | } 19 | } 20 | 21 | const mapDispatchToProps = { 22 | fetchProjects, 23 | setVisibilitySearchText 24 | } 25 | 26 | const ActivePublicLandingPage = connect( 27 | mapStateToProps, 28 | mapDispatchToProps 29 | )(PublicLandingPage) 30 | 31 | export default ActivePublicLandingPage 32 | -------------------------------------------------------------------------------- /lib/public/containers/ActiveUserAccount.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { connect } from 'react-redux' 4 | 5 | import UserAccount from '../components/UserAccount' 6 | import { getAccountTypes } from '../../common/util/user' 7 | import * as feedsActions from '../../manager/actions/feeds' 8 | import * as deploymentsActions from '../../manager/actions/deployments' 9 | import * as projectsActions from '../../manager/actions/projects' 10 | import * as userActions from '../../manager/actions/user' 11 | import * as visibilityFilterActions from '../../manager/actions/visibilityFilter' 12 | import type { AppState, RouterProps } from '../../types/reducers' 13 | 14 | export type Props = RouterProps 15 | 16 | const mapStateToProps = (state: AppState, ownProps: Props) => { 17 | const { projects, user } = state 18 | const { projectId, subpage } = ownProps.routeParams 19 | return { 20 | accountTypes: getAccountTypes(state), 21 | activeComponent: subpage, 22 | projectId, 23 | projects: projects.all, 24 | user, 25 | visibilitySearchText: projects.filter.searchText 26 | } 27 | } 28 | 29 | const mapDispatchToProps = { 30 | fetchProjectDeployments: deploymentsActions.fetchProjectDeployments, 31 | fetchProjectFeeds: feedsActions.fetchProjectFeeds, 32 | fetchProjects: projectsActions.fetchProjects, 33 | sendPasswordReset: userActions.sendPasswordReset, 34 | setVisibilitySearchText: visibilityFilterActions.setVisibilitySearchText, 35 | unsubscribeAll: userActions.unsubscribeAll, 36 | updateTargetForSubscription: userActions.updateTargetForSubscription, 37 | updateUserData: userActions.updateUserData 38 | } 39 | 40 | export default connect(mapStateToProps, mapDispatchToProps)(UserAccount) 41 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: TRANSIT-data-tools Docs 2 | site_url: http://conveyal-data-tools.readthedocs.io 3 | repo_url: https://github.com/conveyal/datatools-manager 4 | docs_dir: docs 5 | site_dir: target/mkdocs 6 | theme: readthedocs 7 | extra_css: [style.css] 8 | 9 | nav: 10 | - Home: 'index.md' 11 | - Data Manager: 12 | - 'Introduction': 'user/introduction.md' 13 | - 'Managing Projects & Feeds': 'user/managing-projects-feeds.md' 14 | - 'Managing Users': 'user/managing-users.md' 15 | - 'GTFS Editor': 16 | - Getting Started: 'user/editor/getting-started.md' 17 | - Stops: 'user/editor/stops.md' 18 | - Routes: 'user/editor/routes.md' 19 | - Patterns: 'user/editor/patterns.md' 20 | - Schedules: 'user/editor/schedules.md' 21 | - Fares: 'user/editor/fares.md' 22 | - Deploying to OTP: 23 | - Overview: 'user/otp-deployment.md' 24 | - Setting up AWS servers: 'user/setting-up-aws-servers.md' 25 | - Adding a deployment server: 'user/add-deployment-server.md' 26 | - Deploying GTFS feeds to OTP: 'user/deploying-feeds.md' 27 | - For Developers: 28 | - Deployment: 'dev/deployment.md' 29 | - Development: 'dev/development.md' 30 | - Migration: 'dev/migration.md' 31 | - Localization: 'dev/localization.md' 32 | - API Interaction: 'dev/api_interaction.md' 33 | - Appendices: 34 | - GTFS Validation Warnings: 'user/appendix-gtfs-warnings.md' 35 | 36 | -------------------------------------------------------------------------------- /patches/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibi-group/datatools-ui/70adc13a79cdfe8c2c6eadafbe2a581a375d26ed/patches/.gitkeep -------------------------------------------------------------------------------- /scripts/load.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # load the database to a fresh server 3 | # usage: load.py dump.json http://localhost:9000 4 | # validation: curl -X POST http://localhost:9000/validateAll (add force=true query param to force validation) 5 | # if validating multiple large feeds, you may need to run application with more GBs, e.g. java -Xmx6G -jar target/datatools.jar 6 | 7 | from sys import argv 8 | import urllib2 9 | 10 | server = argv[2] 11 | # strip trailing slash to normalize url 12 | server = server if not server.endswith('/') else server[:-1] 13 | 14 | # TODO: don't load everything into RAM when loading 15 | inf = open(argv[1]) 16 | dump = inf.read() 17 | 18 | print dump[0:79] 19 | 20 | req = urllib2.Request(server + '/load', dump, {'Content-Type': 'application/json', 'Content-Length': len(dump)}) 21 | opener = urllib2.build_opener() 22 | 23 | try: 24 | opener.open(req) 25 | except urllib2.URLError, e: 26 | print e.reason 27 | print e.read() 28 | -------------------------------------------------------------------------------- /scripts/loadLegacy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # load the database to a fresh server 3 | # usage: loadLegacy.py dump.json http://localhost:9000 4 | 5 | from sys import argv 6 | import urllib2 7 | 8 | server = argv[2] 9 | # strip trailing slash to normalize url 10 | server = server if not server.endswith('/') else server[:-1] 11 | 12 | # TODO: don't load everything into RAM when loading 13 | inf = open(argv[1]) 14 | dump = inf.read() 15 | 16 | print dump[0:79] 17 | 18 | req = urllib2.Request(server + '/loadLegacy', dump, {'Content-Type': 'application/json', 'Content-Length': len(dump)}) 19 | opener = urllib2.build_opener() 20 | 21 | try: 22 | opener.open(req) 23 | except urllib2.URLError, e: 24 | print e.code 25 | print e.read() 26 | -------------------------------------------------------------------------------- /scripts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "auth0": "^2.34.2", 4 | "make-runnable": "^1.3.6", 5 | "request-promise-native": "^1.0.5" 6 | } 7 | } 8 | --------------------------------------------------------------------------------