From 47e657220d2fa4286b271e95d8a0af289953f48a Mon Sep 17 00:00:00 2001 From: Landon Reed Date: Mon, 16 Mar 2020 10:56:31 -0400 Subject: [PATCH 01/48] fix(FeedVersionNavigator): reverse version sort; add retrieval method filtering re #544 --- lib/common/actions/index.js | 4 +- lib/editor/actions/trip.js | 3 + lib/manager/actions/status.js | 114 ++++++++++-------- lib/manager/actions/versions.js | 12 +- .../components/version/FeedVersionDetails.js | 60 ++++++--- .../version/FeedVersionNavigator.js | 29 ++--- .../components/version/FeedVersionReport.js | 3 +- .../version/VersionRetrievalBadge.js | 40 ++++++ .../version/VersionSelectorDropdown.js | 77 ++++++++++++ lib/manager/util/version.js | 4 +- lib/style.css | 1 + lib/types/index.js | 9 +- 12 files changed, 255 insertions(+), 101 deletions(-) create mode 100644 lib/manager/components/version/VersionRetrievalBadge.js create mode 100644 lib/manager/components/version/VersionSelectorDropdown.js diff --git a/lib/common/actions/index.js b/lib/common/actions/index.js index cd7ea23e7..c5c55c360 100644 --- a/lib/common/actions/index.js +++ b/lib/common/actions/index.js @@ -11,7 +11,7 @@ export function createVoidPayloadAction (type: string) { return () => ({ type }) } -export function secureFetch (url: string, method: string = 'get', payload: any, raw: boolean = false, isJSON: boolean = true, actionOnFail: any): any { +export function secureFetch (url: string, method: string = 'get', payload?: any, raw: boolean = false, isJSON: boolean = true, actionOnFail?: string): any { return function (dispatch: dispatchFn, getState: getStateFn) { function consoleError (message) { console.error(`Error making ${method} request to ${url}: `, message) @@ -95,7 +95,7 @@ export function fetchGraphQL ({ }: { errorMessage?: string, query: string, - variables: any + variables?: {[key: string]: string | number | Array} }): any { return function (dispatch: dispatchFn, getState: getStateFn) { const body = { diff --git a/lib/editor/actions/trip.js b/lib/editor/actions/trip.js index 6d9b6e2e8..8b25515c6 100644 --- a/lib/editor/actions/trip.js +++ b/lib/editor/actions/trip.js @@ -92,6 +92,7 @@ export function fetchTripsForCalendar ( ) { return function (dispatch: dispatchFn, getState: getStateFn) { const namespace = getEditorNamespace(feedId, getState()) + if (!namespace) throw new Error('Editor namespace is undefined!') // This fetches patterns on the pattern_id field (rather than ID) because // pattern_id is needed to join on the nested trips table const query = `query ($namespace: String, $pattern_id: [String], $service_id: [String]) { @@ -277,6 +278,7 @@ export function fetchCalendarTripCountsForPattern ( ) { return function (dispatch: dispatchFn, getState: getStateFn) { const namespace = getEditorNamespace(feedId, getState()) + if (!namespace) throw new Error('Editor namespace is undefined!') // This fetches patterns on the pattern_id field (rather than ID) because // pattern_id is needed to join on the nested trips table const query = `query ($namespace: String, $pattern_id: String) { @@ -304,6 +306,7 @@ export function fetchCalendarTripCountsForPattern ( export function fetchTripCounts (feedId: string) { return function (dispatch: dispatchFn, getState: getStateFn) { const namespace = getEditorNamespace(feedId, getState()) + if (!namespace) throw new Error('Editor namespace is undefined!') // This fetches patterns on the pattern_id field (rather than ID) because // pattern_id is needed to join on the nested trips table const query = `query ($namespace: String) { diff --git a/lib/manager/actions/status.js b/lib/manager/actions/status.js index ec29ba76c..3bc2d2605 100644 --- a/lib/manager/actions/status.js +++ b/lib/manager/actions/status.js @@ -8,10 +8,10 @@ import {API_PREFIX} from '../../common/constants' import {isExtensionEnabled} from '../../common/util/config' import { fetchDeployment } from './deployments' import { fetchFeedSource } from './feeds' -import { downloadMergedFeedViaToken } from './projects' +import { downloadMergedFeedViaToken, fetchProjectWithFeeds } from './projects' import { downloadSnapshotViaCredentials } from '../../editor/actions/snapshots' -import type {DataToolsConfig, ServerJob} from '../../types' +import type {DataToolsConfig, MergeFeedsResult, ServerJob} from '../../types' import type {dispatchFn, getStateFn} from '../../types/reducers' type ErrorMessage = { @@ -21,6 +21,13 @@ type ErrorMessage = { title?: string } +type ModalContent = { + action?: any, + body: string, + detail?: any, + title: string +} + export const clearStatusModal = createVoidPayloadAction('CLEAR_STATUS_MODAL') const handlingFinishedJob = createAction( 'HANDLING_FINISHED_JOB', @@ -57,12 +64,7 @@ const setAppInfo = createAction( const setStatusModal = createAction( 'SET_STATUS_MODAL', - (payload: { - action?: any, - body: string, - detail?: any, - title: string - }) => payload + (payload: ModalContent) => payload ) export type StatusActions = ActionType | @@ -134,6 +136,51 @@ export async function fetchAppInfo () { } } +function getMergeFeedModalContent (result: MergeFeedsResult): ModalContent { + const details = [] + // Do nothing or show merged feed modal? Feed version is be created + details.push('Remapped ID count: ' + result.remappedReferences) + if (Object.keys(result.remappedIds).length > 0) { + const remappedIdStrings = [] + for (let key in result.remappedIds) { + // Modify key to remove feed name. + const split = key.split(':') + const tableAndId = split.splice(1, 1) + remappedIdStrings.push(`${tableAndId.join(':')} -> ${result.remappedIds[key]}`) + } + details.push('Remapped IDs: ' + remappedIdStrings.join(', ')) + } + if (result.skippedIds.length > 0) { + const skippedRecordsForTables = {} + result.skippedIds.forEach(id => { + const table = id.split(':')[0] + if (skippedRecordsForTables[table]) { + skippedRecordsForTables[table] = skippedRecordsForTables[table] + 1 + } else { + skippedRecordsForTables[table] = 1 + } + }) + const skippedRecordsStrings = [] + for (let key in skippedRecordsForTables) { + skippedRecordsStrings.push(`${key} - ${skippedRecordsForTables[key]}`) + } + details.push('Skipped records: ' + skippedRecordsStrings.join(', ')) + } + if (result.idConflicts.length > 0) { + // const conflicts = result.idConflicts + details.push('ID conflicts: ' + result.idConflicts.join(', ')) + } + return { + title: result.failed + ? 'Warning: Errors encountered during feed merge!' + : 'Feed merge was successful!', + body: result.failed + ? `Merge failed with ${result.errorCount} errors. ${result.failureReasons.join(', ')}` + : `Merge was completed successfully. A new version will be processed/validated containing the resulting feed.`, + detail: details.join('\n') + } +} + /* eslint-disable complexity */ export function handleFinishedJob (job: ServerJob) { return function (dispatch: dispatchFn, getState: getStateFn) { @@ -203,60 +250,21 @@ export function handleFinishedJob (job: ServerJob) { // If merging feeds for the project, end result is to download zip. if (!job.projectId) { // FIXME use setErrorMessage instead? - console.warn('No project found on job') - return + throw new Error('No project found on job') } - dispatch(downloadMergedFeedViaToken(job.projectId, false)) + // FIXME + dispatch(fetchProjectWithFeeds(job.projectId)) } else { const result = job.mergeFeedsResult - const details = [] if (result) { - // Do nothing or show merged feed modal? Feed version is be created - details.push('Remapped ID count: ' + result.remappedReferences) - if (Object.keys(result.remappedIds).length > 0) { - const remappedIdStrings = [] - for (let key in result.remappedIds) { - // Modify key to remove feed name. - const split = key.split(':') - const tableAndId = split.splice(1, 1) - remappedIdStrings.push(`${tableAndId.join(':')} -> ${result.remappedIds[key]}`) - } - details.push('Remapped IDs: ' + remappedIdStrings.join(', ')) - } - if (result.skippedIds.length > 0) { - const skippedRecordsForTables = {} - result.skippedIds.forEach(id => { - const table = id.split(':')[0] - if (skippedRecordsForTables[table]) { - skippedRecordsForTables[table] = skippedRecordsForTables[table] + 1 - } else { - skippedRecordsForTables[table] = 1 - } - }) - const skippedRecordsStrings = [] - for (let key in skippedRecordsForTables) { - skippedRecordsStrings.push(`${key} - ${skippedRecordsForTables[key]}`) - } - details.push('Skipped records: ' + skippedRecordsStrings.join(', ')) - } - if (result.idConflicts.length > 0) { - // const conflicts = result.idConflicts - details.push('ID conflicts: ' + result.idConflicts.join(', ')) - } - dispatch(setStatusModal({ - title: result.failed - ? 'Warning: Errors encountered during feed merge!' - : 'Feed merge was successful!', - body: result.failed - ? `Merge failed with ${result.errorCount} errors. ${result.failureReasons.join(', ')}` - : `Merge was completed successfully. A new version will be processed/validated containing the resulting feed.`, - detail: details.join('\n') - })) + const modalContent = getMergeFeedModalContent(result) + dispatch(setStatusModal(modalContent)) } } break default: console.warn(`No completion step defined for job type ${job.type}`) + break } } } diff --git a/lib/manager/actions/versions.js b/lib/manager/actions/versions.js index 40c3d82d5..9ebf1f6ac 100644 --- a/lib/manager/actions/versions.js +++ b/lib/manager/actions/versions.js @@ -190,7 +190,7 @@ export function fetchPublicFeedVersions (feedSource: Feed) { /** * Merges two feed versions according to the strategy defined within the */ -export function mergeVersions (targetVersionId: string, versionId: string, mergeType: string) { +export function mergeVersions (targetVersionId: string, versionId: string, mergeType: 'SERVICE_PERIOD' | 'REGIONAL') { return function (dispatch: dispatchFn, getState: getStateFn) { const url = `${SECURE_API_PREFIX}feedversion/merge?feedVersionIds=${targetVersionId},${versionId}&mergeType=${mergeType}` return dispatch(secureFetch(url, 'put')) @@ -316,11 +316,11 @@ export function fetchGTFSEntities ({ } } ` - // If fetching for the editor, cast id to int for csv_line field - return dispatch(fetchGraphQL({ - query, - variables: {namespace, [entityIdField]: editor ? +id : id} - })) + const variables = !id + ? {namespace} + // If fetching a single ID for the editor, cast id to int for csv_line field + : {namespace, [entityIdField]: editor ? +id : id} + return dispatch(fetchGraphQL({query, variables})) .then(data => { dispatch(receiveGTFSEntities({namespace, id, component: type, data, editor, replaceNew})) if (editor) { diff --git a/lib/manager/components/version/FeedVersionDetails.js b/lib/manager/components/version/FeedVersionDetails.js index 17dd31818..fa9426e7a 100644 --- a/lib/manager/components/version/FeedVersionDetails.js +++ b/lib/manager/components/version/FeedVersionDetails.js @@ -18,6 +18,8 @@ import * as versionsActions from '../../actions/versions' import {getConfigProperty, isExtensionEnabled} from '../../../common/util/config' import {BLOCKING_ERROR_TYPES} from '../../util/version' import VersionDateLabel from './VersionDateLabel' +import VersionRetrievalBadge from './VersionRetrievalBadge' +import VersionSelectorDropdown from './VersionSelectorDropdown' import type {FeedVersion, GtfsPlusValidation, Bounds, Feed} from '../../../types' import type {ManagerUserState} from '../../../types/reducers' @@ -51,12 +53,30 @@ export default class FeedVersionDetails extends Component { !!(errorCounts.find(ec => BLOCKING_ERROR_TYPES.indexOf(ec.type) !== -1)) } + _mergeItemFormatter = (v: FeedVersion, i: number, activeVersion: FeedVersion) => { + const name = v.id === activeVersion.id + ? '(Cannot merge with self)' + : v.retrievalMethod === 'SERVICE_PERIOD_MERGE' + ? '(Cannot re-merge feed)' + : v.name + const disabled = v.retrievalMethod === 'SERVICE_PERIOD_MERGE' || + v.id === activeVersion.id + return ( + + {v.version}. {name}{' '} + + + ) + } + _handleMergeVersion = (versionId: string) => { - // For now, merging feeds only works for the MTC extension. It will fail - // when the 'none' merge type is used. - // TODO: add support for other merge types. - const mergeType = isExtensionEnabled('mtc') ? 'MTC' : 'none' - this.props.mergeVersions(this.props.version.id, versionId, mergeType) + // Note: service period feed merge has only been extensively tested with + // MTC-specific logic. + this.props.mergeVersions(this.props.version.id, versionId, 'SERVICE_PERIOD') } _onClickPublish = () => this.props.publishFeedVersion(this.props.version) @@ -81,6 +101,10 @@ export default class FeedVersionDetails extends Component { ) const hasBlockingIssue = this._checkBlockingIssue(version) const hasGtfsPlusBlockingIssue = gtfsPlusValidation && gtfsPlusValidation.issues.length > 0 + const isMergedServicePeriods = version.retrievalMethod === 'SERVICE_PERIOD_MERGE' + const mergeButtonLabel = isMergedServicePeriods + ? 'Cannot re-merge feed' + : 'Merge with version' return (

@@ -89,21 +113,17 @@ export default class FeedVersionDetails extends Component { {// Only show merge feeds button if the feed starts in the future. // FIXME: uncomment out the below to prevent merges with non-future feeds. // moment(summary.startDate).isAfter(moment().startOf('day')) && - Merge with version} - onSelect={this._handleMergeVersion}> - {feedSource.feedVersions && feedSource.feedVersions.map((v, i) => { - if (v.id === version.id) { - return ( - - {v.version}. (Cannot merge with self) - - ) - } - return {v.version}. {v.name} - })} - + {mergeButtonLabel}} + itemFormatter={this._mergeItemFormatter} + version={version} + versions={feedSource.feedVersions} + /> } {/* Version Selector Dropdown */} - - {versions.map((version, k) => { - k = k + 1 - return {k}. {version.name} - })} - + {/* Next Version Button */}

- + {' '} Version published {moment(updated).fromNow()} by {userLink} diff --git a/lib/manager/components/version/VersionRetrievalBadge.js b/lib/manager/components/version/VersionRetrievalBadge.js new file mode 100644 index 000000000..85ed7f773 --- /dev/null +++ b/lib/manager/components/version/VersionRetrievalBadge.js @@ -0,0 +1,40 @@ +// @flow + +import Icon from '@conveyal/woonerf/components/icon' +import * as React from 'react' +import { Badge } from 'react-bootstrap' + +import toSentenceCase from '../../../common/util/to-sentence-case' + +import type { FeedVersion, RetrievalMethod } from '../../../types' + +type Props = {version: FeedVersion} + +const iconForRetrievalMethod = (retrievalMethod: RetrievalMethod) => { + switch (retrievalMethod) { + case 'SERVICE_PERIOD_MERGE': + return 'code-fork' + case 'REGIONAL_MERGE': + return 'globe' + case 'PRODUCED_IN_HOUSE': + return 'pencil' + case 'MANUALLY_UPLOADED': + return 'upload' + case 'FETCHED_AUTOMATICALLY': + return 'cloud-download' + default: + return 'file-archive-o' + } +} + +export default class VersionRetrievalBadge extends React.Component { + render () { + const { version } = this.props + return ( + + ) + } +} diff --git a/lib/manager/components/version/VersionSelectorDropdown.js b/lib/manager/components/version/VersionSelectorDropdown.js new file mode 100644 index 000000000..c30a67538 --- /dev/null +++ b/lib/manager/components/version/VersionSelectorDropdown.js @@ -0,0 +1,77 @@ +// @flow + +import Icon from '@conveyal/woonerf/components/icon' +import * as React from 'react' +import { Badge, Dropdown, MenuItem } from 'react-bootstrap' + +import VersionRetrievalBadge from './VersionRetrievalBadge' + +import type { FeedVersion, RetrievalMethod } from '../../../types' + +type Props = { + dropdownProps: any, + header?: string, + itemFormatter: (FeedVersion, number, FeedVersion) => React.Node, + title: React.Node, + version: FeedVersion, + versions: ?Array +} + +const defaultItemFormatter = (version: FeedVersion, index: number, activeVersion: FeedVersion) => { + return ( + + {version.id === activeVersion.id + ? + : null} + {version.version}. {version.name}{' '} + + + ) +} + +export default class VersionSelectorDropdown extends React.Component { + static defaultProps = { + dropdownProps: { + onSelect: (index: number) => console.log(`selected ${index}`) + }, + itemFormatter: defaultItemFormatter + } + + render () { + const { + dropdownProps, + header, + itemFormatter, + title, + version, + versions + } = this.props + const versionsSorted = versions + ? versions.slice(0).reverse() + : [] + return ( + + + {title} + + + {header? {header} : null} + {versionsSorted.length > 0 + ? versionsSorted.map((v, i) => itemFormatter(v, i, version)) + : No versions available} + + + ) + } +} diff --git a/lib/manager/util/version.js b/lib/manager/util/version.js index 90cdb611a..799a2e1a8 100644 --- a/lib/manager/util/version.js +++ b/lib/manager/util/version.js @@ -167,7 +167,5 @@ export function getVersionValidationSummaryByFilterStrategy ( } export const versionsLastUpdatedComparator = (a: FeedVersion, b: FeedVersion) => { - if (a.updated < b.updated) return -1 - if (a.updated > b.updated) return 1 - return 0 + return a.updated - b.updated } diff --git a/lib/style.css b/lib/style.css index 7ac37040b..c4486989a 100644 --- a/lib/style.css +++ b/lib/style.css @@ -606,6 +606,7 @@ h4.line:after { height: auto; max-height: 400px; overflow-x: hidden; + z-index: 99999 !important; } .manager-header { diff --git a/lib/types/index.js b/lib/types/index.js index ffd7afae9..e794949d0 100644 --- a/lib/types/index.js +++ b/lib/types/index.js @@ -271,7 +271,12 @@ export type Snapshot = { version: number } -export type RetrievalMethod = 'MANUALLY_UPLOADED' | 'FETCHED_AUTOMATICALLY' +export type RetrievalMethod = + 'MANUALLY_UPLOADED' | + 'FETCHED_AUTOMATICALLY' | + 'PRODUCED_IN_HOUSE' | + 'SERVICE_PERIOD_MERGE' | + 'REGIONAL_MERGE' export type Feed = { dateCreated: number, @@ -378,7 +383,7 @@ export type SummarizedFeedVersion = {| export type FeedVersion = {| dateCreated: number, - feedLoadResult: any, // TODO define more exact type + feedLoadResult: FeedLoadResult, feedSource: Feed, feedSourceId?: string, fileSize: number, From a279b5ed9493efbdfb3266b1d06b3ad328231c47 Mon Sep 17 00:00:00 2001 From: Landon Reed Date: Mon, 16 Mar 2020 11:47:31 -0400 Subject: [PATCH 02/48] refactor(VersionRetrievalBadge): account for missing retrieval method --- lib/manager/components/version/VersionRetrievalBadge.js | 7 ++++--- lib/types/index.js | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/manager/components/version/VersionRetrievalBadge.js b/lib/manager/components/version/VersionRetrievalBadge.js index 85ed7f773..9977d9f67 100644 --- a/lib/manager/components/version/VersionRetrievalBadge.js +++ b/lib/manager/components/version/VersionRetrievalBadge.js @@ -10,7 +10,7 @@ import type { FeedVersion, RetrievalMethod } from '../../../types' type Props = {version: FeedVersion} -const iconForRetrievalMethod = (retrievalMethod: RetrievalMethod) => { +const iconForRetrievalMethod = (retrievalMethod: RetrievalMethod | 'UNKNOWN') => { switch (retrievalMethod) { case 'SERVICE_PERIOD_MERGE': return 'code-fork' @@ -30,10 +30,11 @@ const iconForRetrievalMethod = (retrievalMethod: RetrievalMethod) => { export default class VersionRetrievalBadge extends React.Component { render () { const { version } = this.props + const retrievalMethod = version.retrievalMethod || 'UNKNOWN' return ( ) } diff --git a/lib/types/index.js b/lib/types/index.js index e794949d0..ff367dbde 100644 --- a/lib/types/index.js +++ b/lib/types/index.js @@ -400,7 +400,7 @@ export type FeedVersion = {| originNamespace: ?string, previousVersionId: ?string, processedByExternalPublisher: ?number, - retrievalMethod: RetrievalMethod, + retrievalMethod: ?RetrievalMethod, sentToExternalPublisher: ?number, updated: number, user: string, From 4c4be61426e8ff16a4389290909b4f4a24097000 Mon Sep 17 00:00:00 2001 From: Landon Reed Date: Wed, 18 Mar 2020 17:48:58 -0400 Subject: [PATCH 03/48] refactor: fix lint --- lib/manager/actions/status.js | 2 +- lib/manager/components/version/FeedVersionDetails.js | 1 - lib/manager/components/version/VersionRetrievalBadge.js | 1 - lib/manager/components/version/VersionSelectorDropdown.js | 6 +++--- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/manager/actions/status.js b/lib/manager/actions/status.js index 8dfbb638c..ee100474a 100644 --- a/lib/manager/actions/status.js +++ b/lib/manager/actions/status.js @@ -8,7 +8,7 @@ import {API_PREFIX} from '../../common/constants' import {isExtensionEnabled} from '../../common/util/config' import { fetchDeployment } from './deployments' import { fetchFeedSource } from './feeds' -import { downloadMergedFeedViaToken, fetchProjectWithFeeds } from './projects' +import { fetchProjectWithFeeds } from './projects' import { downloadSnapshotViaCredentials } from '../../editor/actions/snapshots' import type {DataToolsConfig, MergeFeedsResult, ServerJob} from '../../types' diff --git a/lib/manager/components/version/FeedVersionDetails.js b/lib/manager/components/version/FeedVersionDetails.js index fa9426e7a..cb1fbde09 100644 --- a/lib/manager/components/version/FeedVersionDetails.js +++ b/lib/manager/components/version/FeedVersionDetails.js @@ -6,7 +6,6 @@ import React, {Component} from 'react' import { Button, ButtonToolbar, - DropdownButton, ListGroupItem, MenuItem } from 'react-bootstrap' diff --git a/lib/manager/components/version/VersionRetrievalBadge.js b/lib/manager/components/version/VersionRetrievalBadge.js index 9977d9f67..1cf7c324b 100644 --- a/lib/manager/components/version/VersionRetrievalBadge.js +++ b/lib/manager/components/version/VersionRetrievalBadge.js @@ -2,7 +2,6 @@ import Icon from '@conveyal/woonerf/components/icon' import * as React from 'react' -import { Badge } from 'react-bootstrap' import toSentenceCase from '../../../common/util/to-sentence-case' diff --git a/lib/manager/components/version/VersionSelectorDropdown.js b/lib/manager/components/version/VersionSelectorDropdown.js index c30a67538..a6681010a 100644 --- a/lib/manager/components/version/VersionSelectorDropdown.js +++ b/lib/manager/components/version/VersionSelectorDropdown.js @@ -2,11 +2,11 @@ import Icon from '@conveyal/woonerf/components/icon' import * as React from 'react' -import { Badge, Dropdown, MenuItem } from 'react-bootstrap' +import { Dropdown, MenuItem } from 'react-bootstrap' import VersionRetrievalBadge from './VersionRetrievalBadge' -import type { FeedVersion, RetrievalMethod } from '../../../types' +import type { FeedVersion } from '../../../types' type Props = { dropdownProps: any, @@ -66,7 +66,7 @@ export default class VersionSelectorDropdown extends React.Component { {title} - {header? {header} : null} + {header ? {header} : null} {versionsSorted.length > 0 ? versionsSorted.map((v, i) => itemFormatter(v, i, version)) : No versions available} From e8ec5f58b493f5d4960834b1a0f614f4a53c4092 Mon Sep 17 00:00:00 2001 From: Landon Reed Date: Fri, 20 Mar 2020 15:35:38 -0400 Subject: [PATCH 04/48] refactor: address PR comments --- lib/manager/actions/status.js | 7 +- .../components/version/FeedVersionDetails.js | 17 +-- .../version/VersionRetrievalBadge.js | 22 ++-- .../version/VersionSelectorDropdown.js | 100 +++++++++--------- 4 files changed, 70 insertions(+), 76 deletions(-) diff --git a/lib/manager/actions/status.js b/lib/manager/actions/status.js index ee100474a..e53eec23e 100644 --- a/lib/manager/actions/status.js +++ b/lib/manager/actions/status.js @@ -154,11 +154,8 @@ function getMergeFeedModalContent (result: MergeFeedsResult): ModalContent { const skippedRecordsForTables = {} result.skippedIds.forEach(id => { const table = id.split(':')[0] - if (skippedRecordsForTables[table]) { - skippedRecordsForTables[table] = skippedRecordsForTables[table] + 1 - } else { - skippedRecordsForTables[table] = 1 - } + // Increment count of skipped records for each value found per table. + skippedRecordsForTables[table] = (skippedRecordsForTables[table] || 0) + 1 }) const skippedRecordsStrings = [] for (let key in skippedRecordsForTables) { diff --git a/lib/manager/components/version/FeedVersionDetails.js b/lib/manager/components/version/FeedVersionDetails.js index cb1fbde09..c61e7c0a5 100644 --- a/lib/manager/components/version/FeedVersionDetails.js +++ b/lib/manager/components/version/FeedVersionDetails.js @@ -53,13 +53,16 @@ export default class FeedVersionDetails extends Component { } _mergeItemFormatter = (v: FeedVersion, i: number, activeVersion: FeedVersion) => { - const name = v.id === activeVersion.id - ? '(Cannot merge with self)' - : v.retrievalMethod === 'SERVICE_PERIOD_MERGE' - ? '(Cannot re-merge feed)' - : v.name - const disabled = v.retrievalMethod === 'SERVICE_PERIOD_MERGE' || - v.id === activeVersion.id + let name = v.name + let disabled = false + if (v.retrievalMethod === 'SERVICE_PERIOD_MERGE') { + name = '(Cannot re-merge feed)' + disabled = true + } + if (v.id === activeVersion.id) { + name = '(Cannot merge with self)' + disabled = true + } return ( } } -export default class VersionRetrievalBadge extends React.Component { - render () { - const { version } = this.props - const retrievalMethod = version.retrievalMethod || 'UNKNOWN' - return ( - - ) - } +export default function VersionRetrievalBadge (props: Props) { + const { version } = props + const retrievalMethod = version.retrievalMethod || 'UNKNOWN' + return ( + + ) } diff --git a/lib/manager/components/version/VersionSelectorDropdown.js b/lib/manager/components/version/VersionSelectorDropdown.js index a6681010a..921ca7ac5 100644 --- a/lib/manager/components/version/VersionSelectorDropdown.js +++ b/lib/manager/components/version/VersionSelectorDropdown.js @@ -17,61 +17,57 @@ type Props = { versions: ?Array } -const defaultItemFormatter = (version: FeedVersion, index: number, activeVersion: FeedVersion) => { +const DefaultItemFormatter = (version: FeedVersion, activeVersion: FeedVersion) => ( + + {version.id === activeVersion.id + ? + : null} + {version.version}. {version.name}{' '} + + +) + +export default function VersionSelectorDropdown (props: Props) { + const { + dropdownProps, + header, + itemFormatter, + title, + version, + versions + } = props + const versionsSorted = versions + ? versions.slice(0).reverse() + : [] return ( - - {version.id === activeVersion.id - ? - : null} - {version.version}. {version.name}{' '} - - + + {title} + + + {header ? {header} : null} + {versionsSorted.length > 0 + ? versionsSorted.map((v, i) => itemFormatter(v, i, version)) + : No versions available} + + ) } -export default class VersionSelectorDropdown extends React.Component { - static defaultProps = { - dropdownProps: { - onSelect: (index: number) => console.log(`selected ${index}`) - }, - itemFormatter: defaultItemFormatter - } - - render () { - const { - dropdownProps, - header, - itemFormatter, - title, - version, - versions - } = this.props - const versionsSorted = versions - ? versions.slice(0).reverse() - : [] - return ( - - - {title} - - - {header ? {header} : null} - {versionsSorted.length > 0 - ? versionsSorted.map((v, i) => itemFormatter(v, i, version)) - : No versions available} - - - ) - } +VersionSelectorDropdown.defaultProps = { + dropdownProps: { + onSelect: (key: number | string) => console.log(`selected ${key}`) + }, + itemFormatter: DefaultItemFormatter } From f1797a76b777646b5e82b4952503c542f40ac450 Mon Sep 17 00:00:00 2001 From: Landon Reed Date: Thu, 26 Mar 2020 10:54:51 -0400 Subject: [PATCH 05/48] refactor: fix flow --- lib/manager/components/version/FeedVersionDetails.js | 2 +- lib/manager/components/version/VersionSelectorDropdown.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/manager/components/version/FeedVersionDetails.js b/lib/manager/components/version/FeedVersionDetails.js index c61e7c0a5..b06c42e52 100644 --- a/lib/manager/components/version/FeedVersionDetails.js +++ b/lib/manager/components/version/FeedVersionDetails.js @@ -52,7 +52,7 @@ export default class FeedVersionDetails extends Component { !!(errorCounts.find(ec => BLOCKING_ERROR_TYPES.indexOf(ec.type) !== -1)) } - _mergeItemFormatter = (v: FeedVersion, i: number, activeVersion: FeedVersion) => { + _mergeItemFormatter = (v: FeedVersion, activeVersion: FeedVersion) => { let name = v.name let disabled = false if (v.retrievalMethod === 'SERVICE_PERIOD_MERGE') { diff --git a/lib/manager/components/version/VersionSelectorDropdown.js b/lib/manager/components/version/VersionSelectorDropdown.js index 921ca7ac5..5461acc32 100644 --- a/lib/manager/components/version/VersionSelectorDropdown.js +++ b/lib/manager/components/version/VersionSelectorDropdown.js @@ -11,7 +11,7 @@ import type { FeedVersion } from '../../../types' type Props = { dropdownProps: any, header?: string, - itemFormatter: (FeedVersion, number, FeedVersion) => React.Node, + itemFormatter: (FeedVersion, FeedVersion) => React.Node, title: React.Node, version: FeedVersion, versions: ?Array @@ -58,7 +58,7 @@ export default function VersionSelectorDropdown (props: Props) { {header ? {header} : null} {versionsSorted.length > 0 - ? versionsSorted.map((v, i) => itemFormatter(v, i, version)) + ? versionsSorted.map((v, i) => itemFormatter(v, version)) : No versions available} From 18702dd65ade982583208eaedab41bf686afb6b9 Mon Sep 17 00:00:00 2001 From: Landon Reed Date: Mon, 6 Apr 2020 11:49:42 -0400 Subject: [PATCH 06/48] fix(gtfs.yml): add more currency types for fares fixes #550 --- gtfs.yml | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/gtfs.yml b/gtfs.yml index 1ffe654b6..45fe8418e 100644 --- a/gtfs.yml +++ b/gtfs.yml @@ -629,9 +629,28 @@ inputType: DROPDOWN bulkEditEnabled: true options: - - value: 'USD' - - value: 'EUR' - - value: 'GBP' + - value: USD + text: US dollar (USD) + - value: AUD + text: Australian dollar (AUD) + - value: CAD + text: Canadian dollar (CAD) + - value: CHF + text: Swiss franc (CHF) + - value: CNH + text: Chinese renminbi (CNH) + - value: EUR + text: Euro (EUR) + - value: GBP + text: Pound sterling (GBP) + - value: JPY + text: Japanese yen (JPY) + - value: MXN + text: Mexican peso (MXN) + - value: NZD + text: New Zealand dollar (NZD) + - value: SEK + text: Swedish krona (SEK) columnWidth: 12 helpContent: "The currency_type field defines the currency used to pay the fare. Please use the ISO 4217 alphabetical currency codes which can be found at the following URL:http://en.wikipedia.org/wiki/ISO_4217." - name: "payment_method" From 5fb9ec97a10b0a555254caa841c84faeb4112816 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Fri, 10 Apr 2020 13:06:14 -0400 Subject: [PATCH 07/48] feat(GtfsPlusVersionSummary): Add GTFS+ validation issue details. --- .../components/GtfsPlusVersionSummary.js | 186 ++++++++++++++---- 1 file changed, 144 insertions(+), 42 deletions(-) diff --git a/lib/gtfsplus/components/GtfsPlusVersionSummary.js b/lib/gtfsplus/components/GtfsPlusVersionSummary.js index b794f0345..d3a69dcd1 100644 --- a/lib/gtfsplus/components/GtfsPlusVersionSummary.js +++ b/lib/gtfsplus/components/GtfsPlusVersionSummary.js @@ -1,16 +1,33 @@ // @flow +import moment from 'moment' import React, {Component} from 'react' -import {Panel, Row, Col, Table, Button, ButtonToolbar, Glyphicon, Alert} from 'react-bootstrap' +import { + Alert, + Button, + ButtonToolbar, + Col, + Glyphicon, + Label, + Panel, + Row, + Table +} from 'react-bootstrap' import {browserHistory, Link} from 'react-router' -import moment from 'moment' -import {getGtfsPlusSpec} from '../../common/util/config' import * as gtfsPlusActions from '../actions/gtfsplus' - +import {getGtfsPlusSpec} from '../../common/util/config' import type {Props as ContainerProps} from '../containers/ActiveGtfsPlusVersionSummary' +import type {GtfsSpecTable} from '../../types' import type {GtfsPlusReducerState, ManagerUserState} from '../../types/reducers' +type Issue = { + description: string, + fieldName: string, + rowIndex: number, + tableId: string +} + type Props = ContainerProps & { deleteGtfsPlusFeed: typeof gtfsPlusActions.deleteGtfsPlusFeed, downloadGtfsPlusFeed: typeof gtfsPlusActions.downloadGtfsPlusFeed, @@ -21,11 +38,17 @@ type Props = ContainerProps & { } type State = { - expanded: boolean + expanded: boolean, + tableExpanded: any } +type IssueFilter = Issue => boolean + export default class GtfsPlusVersionSummary extends Component { - state = { expanded: false } + state = { + expanded: false, + tableExpanded: {} + } componentDidMount () { this.props.downloadGtfsPlusFeed(this.props.version.id) @@ -51,13 +74,19 @@ export default class GtfsPlusVersionSummary extends Component { return issuesForTable[tableId].length.toLocaleString() } - _getTableLevelIssues = (tableId: string) => { + _getTableLevelIssues = (tableId: string): ?Array => { + return this._getIssues(tableId, issue => issue.rowIndex === -1) + } + + _getIssues = (tableId: string, filter: ?IssueFilter): ?Array => { const {issuesForTable} = this.props if (!issuesForTable) return null if (!(tableId in issuesForTable)) return null - // Table level issues are identified by not having -1 for row index. - const tableLevelIssues = issuesForTable[tableId].filter(issue => issue.rowIndex === -1) - return tableLevelIssues.length > 0 ? tableLevelIssues : null + + // Filter table level issues or row issues using the specified filter. + filter = filter || (() => true) + const issues = issuesForTable[tableId].filter(filter) + return (issues.length > 0 ? issues : null) } feedStatus () { @@ -95,6 +124,68 @@ export default class GtfsPlusVersionSummary extends Component { this.setState({ expanded: !expanded }) } + _toggleTableExpanded = (tableName: string): void => { + const { tableExpanded } = this.state + const newTableExpanded = Object.assign(tableExpanded) + newTableExpanded[tableName] = !newTableExpanded[tableName] + + this.setState({ tableExpanded: newTableExpanded }) + } + + renderIssues = (table: GtfsSpecTable) => { + const { tableExpanded } = this.state + const isExpanded = tableExpanded[table.name] + const issueCount = this.validationIssueCount(table.id) + const tableLevelIssues = this._getTableLevelIssues(table.id) + const allIssues = this._getIssues(table.id) + allIssues && allIssues.sort( + (issue1, issue2) => issue1.rowIndex - issue2.rowIndex + ) + + return ( +
+ + + + {isExpanded && + + + + + + + + + {allIssues && allIssues.map((issue, index) => + + {/* This is the line number in the file */} + + + + )} + +
LineColumnIssue
{issue.rowIndex + 2}{issue.fieldName} + {issue.description} + {' '} + {issue.rowIndex === -1 && } +
} +
+
+ ) + } + render () { const { gtfsplus, @@ -183,51 +274,62 @@ export default class GtfsPlusVersionSummary extends Component { - +
- - - {getGtfsPlusSpec().map((table, index) => { + {/* FIXME: reinstate this after switching to React 16. */} + {/** + * Change the behavior as follows: + * - Table-level issues are still critical and blocking and and displayed in red. + * - Per-row issues are still amber warnings and non-blocking, + * but will now be displayed individually instead of being aggregated. + * Maybe only display the first 25 issues to avoid long rendering times??? + * - Issues are displayed on a full-width sub-table for better readability, + * in the same "row" as the issue summary. + * - Tables are sorted alphabetically. + */} + {getGtfsPlusSpec() + .sort((table1, table2) => table1.name.localeCompare(table2.name)) + .map((table, index) => { const issueCount = this.validationIssueCount(table.id) const tableLevelIssues = this._getTableLevelIssues(table.id) + const hasIssues = +issueCount > 0 + const className = tableLevelIssues + ? 'danger' + : (hasIssues ? 'warning' : '') + return ( - 0 && 'warning' - } - style={{ color: this.isTableIncluded(table.id) === 'Yes' ? 'black' : 'lightGray' }}> - - - - - + // FIXME: Use (React 16+ only.) + + + + + + + + {hasIssues && ( + + + + )} + + // ) })} - + {/* */}
Table Included? Records Validation Issues
- {table.name} - {tableLevelIssues - ? -
- {tableLevelIssues.length} critical table issue(s): -
    - {tableLevelIssues.map((issue, i) => -
  • {issue.description}
  • )} -
-
- : null - } -
{this.isTableIncluded(table.id)}{this.tableRecordCount(table.id)}{issueCount} -
+ {table.name} + {this.isTableIncluded(table.id)}{this.tableRecordCount(table.id)}{issueCount}
+ {this.renderIssues(table)} +
From f2ddd8189c465e258575ef21f332473f53b75af8 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Fri, 10 Apr 2020 14:41:44 -0400 Subject: [PATCH 08/48] improvement(GtfsPlusVersionSummary): Remove top boder in issues cell. Remove issue table botton marg --- lib/gtfsplus/components/GtfsPlusVersionSummary.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/gtfsplus/components/GtfsPlusVersionSummary.js b/lib/gtfsplus/components/GtfsPlusVersionSummary.js index d3a69dcd1..3d4b23963 100644 --- a/lib/gtfsplus/components/GtfsPlusVersionSummary.js +++ b/lib/gtfsplus/components/GtfsPlusVersionSummary.js @@ -159,7 +159,7 @@ export default class GtfsPlusVersionSummary extends Component { {tableLevelIssues && issueCount && ` (${tableLevelIssues.length}/${issueCount} blocking)`} - {isExpanded && + {isExpanded &&
@@ -311,16 +311,14 @@ export default class GtfsPlusVersionSummary extends Component { className={className} rowSpan={hasIssues ? 2 : 1} style={{ color: this.isTableIncluded(table.id) === 'Yes' ? 'black' : 'lightGray' }}> - + {hasIssues && ( - From ade917adfd1c83882323dd31ad8ded7242368abc Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Fri, 10 Apr 2020 16:18:44 -0400 Subject: [PATCH 09/48] fix(GtfsPlusVersionSummary): Tame down changes --- .../components/GtfsPlusVersionSummary.js | 157 +++++------------- 1 file changed, 37 insertions(+), 120 deletions(-) diff --git a/lib/gtfsplus/components/GtfsPlusVersionSummary.js b/lib/gtfsplus/components/GtfsPlusVersionSummary.js index 3d4b23963..b8d031864 100644 --- a/lib/gtfsplus/components/GtfsPlusVersionSummary.js +++ b/lib/gtfsplus/components/GtfsPlusVersionSummary.js @@ -8,7 +8,6 @@ import { ButtonToolbar, Col, Glyphicon, - Label, Panel, Row, Table @@ -18,7 +17,6 @@ import {browserHistory, Link} from 'react-router' import * as gtfsPlusActions from '../actions/gtfsplus' import {getGtfsPlusSpec} from '../../common/util/config' import type {Props as ContainerProps} from '../containers/ActiveGtfsPlusVersionSummary' -import type {GtfsSpecTable} from '../../types' import type {GtfsPlusReducerState, ManagerUserState} from '../../types/reducers' type Issue = { @@ -37,17 +35,11 @@ type Props = ContainerProps & { user: ManagerUserState } -type State = { - expanded: boolean, - tableExpanded: any -} - -type IssueFilter = Issue => boolean +type State = { expanded: boolean } export default class GtfsPlusVersionSummary extends Component { state = { - expanded: false, - tableExpanded: {} + expanded: false } componentDidMount () { @@ -75,18 +67,12 @@ export default class GtfsPlusVersionSummary extends Component { } _getTableLevelIssues = (tableId: string): ?Array => { - return this._getIssues(tableId, issue => issue.rowIndex === -1) - } - - _getIssues = (tableId: string, filter: ?IssueFilter): ?Array => { const {issuesForTable} = this.props if (!issuesForTable) return null if (!(tableId in issuesForTable)) return null - - // Filter table level issues or row issues using the specified filter. - filter = filter || (() => true) - const issues = issuesForTable[tableId].filter(filter) - return (issues.length > 0 ? issues : null) + // Table level issues are identified by not having -1 for row index. + const tableLevelIssues = issuesForTable[tableId].filter(issue => issue.rowIndex === -1) + return tableLevelIssues.length > 0 ? tableLevelIssues : null } feedStatus () { @@ -124,68 +110,6 @@ export default class GtfsPlusVersionSummary extends Component { this.setState({ expanded: !expanded }) } - _toggleTableExpanded = (tableName: string): void => { - const { tableExpanded } = this.state - const newTableExpanded = Object.assign(tableExpanded) - newTableExpanded[tableName] = !newTableExpanded[tableName] - - this.setState({ tableExpanded: newTableExpanded }) - } - - renderIssues = (table: GtfsSpecTable) => { - const { tableExpanded } = this.state - const isExpanded = tableExpanded[table.name] - const issueCount = this.validationIssueCount(table.id) - const tableLevelIssues = this._getTableLevelIssues(table.id) - const allIssues = this._getIssues(table.id) - allIssues && allIssues.sort( - (issue1, issue2) => issue1.rowIndex - issue2.rowIndex - ) - - return ( -
- - - - {isExpanded &&
Line - {table.name} - {table.name} {this.isTableIncluded(table.id)} {this.tableRecordCount(table.id)} {issueCount}
+ {this.renderIssues(table)}
- - - - - - - - - {allIssues && allIssues.map((issue, index) => - - {/* This is the line number in the file */} - - - - )} - -
LineColumnIssue
{issue.rowIndex + 2}{issue.fieldName} - {issue.description} - {' '} - {issue.rowIndex === -1 && } -
} - - - ) - } - render () { const { gtfsplus, @@ -274,7 +198,7 @@ export default class GtfsPlusVersionSummary extends Component { - +
@@ -283,51 +207,44 @@ export default class GtfsPlusVersionSummary extends Component { - {/* FIXME: reinstate this after switching to React 16. */} - {/** - * Change the behavior as follows: - * - Table-level issues are still critical and blocking and and displayed in red. - * - Per-row issues are still amber warnings and non-blocking, - * but will now be displayed individually instead of being aggregated. - * Maybe only display the first 25 issues to avoid long rendering times??? - * - Issues are displayed on a full-width sub-table for better readability, - * in the same "row" as the issue summary. - * - Tables are sorted alphabetically. - */} - {getGtfsPlusSpec() - .sort((table1, table2) => table1.name.localeCompare(table2.name)) - .map((table, index) => { - const issueCount = this.validationIssueCount(table.id) - const tableLevelIssues = this._getTableLevelIssues(table.id) - const hasIssues = +issueCount > 0 - const className = tableLevelIssues - ? 'danger' - : (hasIssues ? 'warning' : '') + + {getGtfsPlusSpec() + .sort((table1, table2) => table1.name.localeCompare(table2.name)) + .map((table, index) => { + const issueCount = this.validationIssueCount(table.id) + const tableLevelIssues = this._getTableLevelIssues(table.id) - return ( - // FIXME: Use (React 16+ only.) - + return ( 0 && 'warning' + } style={{ color: this.isTableIncluded(table.id) === 'Yes' ? 'black' : 'lightGray' }}> - + + - {hasIssues && ( - - - - )} - - // - ) - })} - {/* */} + ) + })} +
TableValidation Issues
{table.name} + {table.name} + {tableLevelIssues + ? +
+ {tableLevelIssues.length} critical table issue(s): +
    + {tableLevelIssues.map((issue, i) => +
  • {issue.fieldName}: {issue.description}
  • )} +
+
+ : null + } +
{this.isTableIncluded(table.id)} {this.tableRecordCount(table.id)} {issueCount}
- {this.renderIssues(table)} -
From 4c3f49ff28814242014eb99865f579502f17c86f Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Fri, 10 Apr 2020 16:24:41 -0400 Subject: [PATCH 10/48] style(GtfsPlusVersionSummary): Revert some code layout. --- lib/gtfsplus/components/GtfsPlusVersionSummary.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/gtfsplus/components/GtfsPlusVersionSummary.js b/lib/gtfsplus/components/GtfsPlusVersionSummary.js index b8d031864..2e621bcec 100644 --- a/lib/gtfsplus/components/GtfsPlusVersionSummary.js +++ b/lib/gtfsplus/components/GtfsPlusVersionSummary.js @@ -35,12 +35,12 @@ type Props = ContainerProps & { user: ManagerUserState } -type State = { expanded: boolean } +type State = { + expanded: boolean +} export default class GtfsPlusVersionSummary extends Component { - state = { - expanded: false - } + state = { expanded: false } componentDidMount () { this.props.downloadGtfsPlusFeed(this.props.version.id) From bd14749796a07d6178bbdf024fd204a04057ca0c Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Fri, 10 Apr 2020 16:42:00 -0400 Subject: [PATCH 11/48] fix(GtfsPlusVersionSummary): Address PR comments --- lib/gtfsplus/components/GtfsPlusVersionSummary.js | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/lib/gtfsplus/components/GtfsPlusVersionSummary.js b/lib/gtfsplus/components/GtfsPlusVersionSummary.js index 2e621bcec..96a111ed9 100644 --- a/lib/gtfsplus/components/GtfsPlusVersionSummary.js +++ b/lib/gtfsplus/components/GtfsPlusVersionSummary.js @@ -15,17 +15,12 @@ import { import {browserHistory, Link} from 'react-router' import * as gtfsPlusActions from '../actions/gtfsplus' + import {getGtfsPlusSpec} from '../../common/util/config' import type {Props as ContainerProps} from '../containers/ActiveGtfsPlusVersionSummary' +import type {GtfsPlusValidationIssue} from '../../types' import type {GtfsPlusReducerState, ManagerUserState} from '../../types/reducers' -type Issue = { - description: string, - fieldName: string, - rowIndex: number, - tableId: string -} - type Props = ContainerProps & { deleteGtfsPlusFeed: typeof gtfsPlusActions.deleteGtfsPlusFeed, downloadGtfsPlusFeed: typeof gtfsPlusActions.downloadGtfsPlusFeed, @@ -66,7 +61,7 @@ export default class GtfsPlusVersionSummary extends Component { return issuesForTable[tableId].length.toLocaleString() } - _getTableLevelIssues = (tableId: string): ?Array => { + _getTableLevelIssues = (tableId: string): ?Array => { const {issuesForTable} = this.props if (!issuesForTable) return null if (!(tableId in issuesForTable)) return null From 1820fc54173c042e044a4ebebe00be4f1c4b1015 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Mon, 13 Apr 2020 10:34:39 -0400 Subject: [PATCH 12/48] fix(getGtfsPlusSpec): Move sorting from GtfsPlusVersionSummary to getGtfsPlusSpec(). --- lib/common/util/config.js | 2 +- .../components/GtfsPlusVersionSummary.js | 68 +++++++++---------- 2 files changed, 34 insertions(+), 36 deletions(-) diff --git a/lib/common/util/config.js b/lib/common/util/config.js index 5c94779c6..4f39d28fd 100644 --- a/lib/common/util/config.js +++ b/lib/common/util/config.js @@ -37,7 +37,7 @@ export function getGtfsPlusSpec (): Array { const CONFIG: DataToolsConfig = window.DT_CONFIG const GTFS_PLUS_SPEC = CONFIG.specifications.gtfsplus if (!GTFS_PLUS_SPEC) throw new Error('GTFS+ yml configuration file is not defined!') - return GTFS_PLUS_SPEC + return GTFS_PLUS_SPEC.sort((table1, table2) => table1.name.localeCompare(table2.name)) } /** diff --git a/lib/gtfsplus/components/GtfsPlusVersionSummary.js b/lib/gtfsplus/components/GtfsPlusVersionSummary.js index 96a111ed9..021e3305b 100644 --- a/lib/gtfsplus/components/GtfsPlusVersionSummary.js +++ b/lib/gtfsplus/components/GtfsPlusVersionSummary.js @@ -203,42 +203,40 @@ export default class GtfsPlusVersionSummary extends Component { - {getGtfsPlusSpec() - .sort((table1, table2) => table1.name.localeCompare(table2.name)) - .map((table, index) => { - const issueCount = this.validationIssueCount(table.id) - const tableLevelIssues = this._getTableLevelIssues(table.id) - - return ( - 0 && 'warning' + {getGtfsPlusSpec().map((table, index) => { + const issueCount = this.validationIssueCount(table.id) + const tableLevelIssues = this._getTableLevelIssues(table.id) + + return ( + 0 && 'warning' + } + style={{ color: this.isTableIncluded(table.id) === 'Yes' ? 'black' : 'lightGray' }}> + + {table.name} + {tableLevelIssues + ? +
+ {tableLevelIssues.length} critical table issue(s): +
    + {tableLevelIssues.map((issue, i) => +
  • {issue.fieldName}: {issue.description}
  • )} +
+
+ : null } - style={{ color: this.isTableIncluded(table.id) === 'Yes' ? 'black' : 'lightGray' }}> - - {table.name} - {tableLevelIssues - ? -
- {tableLevelIssues.length} critical table issue(s): -
    - {tableLevelIssues.map((issue, i) => -
  • {issue.fieldName}: {issue.description}
  • )} -
-
- : null - } - - {this.isTableIncluded(table.id)} - {this.tableRecordCount(table.id)} - {issueCount} - - - ) - })} + + {this.isTableIncluded(table.id)} + {this.tableRecordCount(table.id)} + {issueCount} + + + ) + })} From 5c65a19ad9b144553ec06cd2c279d6f92d4d0cc2 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Mon, 13 Apr 2020 10:41:02 -0400 Subject: [PATCH 13/48] style(GtfsPlusVersionSummary): Adjust code layout. --- lib/gtfsplus/components/GtfsPlusVersionSummary.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/gtfsplus/components/GtfsPlusVersionSummary.js b/lib/gtfsplus/components/GtfsPlusVersionSummary.js index 021e3305b..460bfe156 100644 --- a/lib/gtfsplus/components/GtfsPlusVersionSummary.js +++ b/lib/gtfsplus/components/GtfsPlusVersionSummary.js @@ -200,13 +200,13 @@ export default class GtfsPlusVersionSummary extends Component { Included? Records Validation Issues + {getGtfsPlusSpec().map((table, index) => { const issueCount = this.validationIssueCount(table.id) const tableLevelIssues = this._getTableLevelIssues(table.id) - return ( Date: Fri, 20 Mar 2020 16:06:44 -0400 Subject: [PATCH 14/48] fix(gtfs+): change rider_category_id from Adult -> Regular re #546 --- gtfsplus.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gtfsplus.yml b/gtfsplus.yml index e486909b4..db9cc4341 100644 --- a/gtfsplus.yml +++ b/gtfsplus.yml @@ -210,7 +210,7 @@ inputType: DROPDOWN options: - value: '1' - text: Adult + text: Regular - value: '2' text: Senior - value: '3' From 388647cb6074c32bee011c827161729b3118f2f5 Mon Sep 17 00:00:00 2001 From: Landon Reed Date: Wed, 27 May 2020 15:58:54 -0400 Subject: [PATCH 15/48] feat(gtfs-transform): add feed transformation settings re #544 --- gtfs.yml | 1 + lib/common/constants/index.js | 12 + lib/manager/components/FeedSourceSettings.js | 213 ++------- .../components/FeedTransformationSettings.js | 423 ++++++++++++++++++ lib/manager/components/GeneralSettings.js | 202 +++++++++ .../version/VersionRetrievalBadge.js | 15 +- .../version/VersionSelectorDropdown.js | 9 +- lib/types/index.js | 4 +- 8 files changed, 692 insertions(+), 187 deletions(-) create mode 100644 lib/manager/components/FeedTransformationSettings.js create mode 100644 lib/manager/components/GeneralSettings.js diff --git a/gtfs.yml b/gtfs.yml index 1ffe654b6..463c673bf 100644 --- a/gtfs.yml +++ b/gtfs.yml @@ -776,6 +776,7 @@ - id: scheduleexception name: (none) + datatools: true helpContent: Conveyal-specific table for classifying schedule exceptions. fields: - name: name diff --git a/lib/common/constants/index.js b/lib/common/constants/index.js index cb78e6f29..dbba8df02 100644 --- a/lib/common/constants/index.js +++ b/lib/common/constants/index.js @@ -1,4 +1,7 @@ // @flow + +import type {RetrievalMethod} from '../../types' + const SECURE: string = 'secure/' export const API_PREFIX: string = `/api/manager/` export const SECURE_API_PREFIX: string = `${API_PREFIX}${SECURE}` @@ -9,3 +12,12 @@ export const DEFAULT_DESCRIPTION = 'A command center for managing, editing, vali export const DEFAULT_LOGO = 'https://d2tyb7byn1fef9.cloudfront.net/ibi_group_black-512x512.png' export const DEFAULT_LOGO_SMALL = 'https://d2tyb7byn1fef9.cloudfront.net/ibi_group-128x128.png' export const DEFAULT_TITLE = 'Data Tools' + +export const RETRIEVAL_METHODS: Array = [ + 'MANUALLY_UPLOADED', + 'FETCHED_AUTOMATICALLY', + 'PRODUCED_IN_HOUSE', + 'SERVICE_PERIOD_MERGE', + 'REGIONAL_MERGE', + 'VERSION_CLONE' +] diff --git a/lib/manager/components/FeedSourceSettings.js b/lib/manager/components/FeedSourceSettings.js index a8d95bb92..36cbc503c 100644 --- a/lib/manager/components/FeedSourceSettings.js +++ b/lib/manager/components/FeedSourceSettings.js @@ -1,13 +1,20 @@ // @flow -import Icon from '@conveyal/woonerf/components/icon' import React, {Component} from 'react' -import {Col, Row, ListGroup, ListGroupItem, Button, Panel, FormControl, InputGroup, ControlLabel, FormGroup, Checkbox} from 'react-bootstrap' +import { + Col, + ListGroup, + ListGroupItem, + Panel, + Row +} from 'react-bootstrap' import {LinkContainer} from 'react-router-bootstrap' import * as feedsActions from '../actions/feeds' import toSentenceCase from '../../common/util/to-sentence-case' import ExternalPropertiesTable from './ExternalPropertiesTable' +import FeedTransformationSettings from './FeedTransformationSettings' +import GeneralSettings from './GeneralSettings' import type {Feed, Project} from '../../types' import type {ManagerUserState} from '../../types/reducers' @@ -23,85 +30,24 @@ type Props = { user: ManagerUserState } -type State = { - name?: ?string, - url?: ?string -} - -export default class FeedSourceSettings extends Component { - state = {} - - _onChange = ({target}: SyntheticInputEvent) => { - // Change empty string to null to avoid setting URL to empty string value. - const value = target.value === '' ? null : target.value.trim() - this.setState({[target.name]: value}) - } - - _onToggleDeployable = () => { - const {feedSource, updateFeedSource} = this.props - updateFeedSource(feedSource, {deployable: !feedSource.deployable}) - } - - _getFormValue = (key: 'name' | 'url') => { - // If state value does not exist (i.e., form is unedited), revert to value - // from props. - const value = typeof this.state[key] === 'undefined' - ? this.props.feedSource[key] - : this.state[key] - // Revert to empty string to avoid console error with null value for form. - return value || '' - } - - _onToggleAutoFetch = () => { - const {feedSource, updateFeedSource} = this.props - const value = feedSource.retrievalMethod === 'FETCHED_AUTOMATICALLY' - ? 'MANUALLY_UPLOADED' - : 'FETCHED_AUTOMATICALLY' - updateFeedSource(feedSource, {retrievalMethod: value}) - } - - _onTogglePublic = () => { - const {feedSource, updateFeedSource} = this.props - updateFeedSource(feedSource, {isPublic: !feedSource.isPublic}) - } - - _onNameChanged = (evt: SyntheticInputEvent) => { - this.setState({name: evt.target.value}) - } - - _onNameSaved = () => { - const {feedSource, updateFeedSource} = this.props - updateFeedSource(feedSource, {name: this.state.name}) - } - - _onSaveUrl = () => { - const {feedSource, updateFeedSource} = this.props - updateFeedSource(feedSource, {url: this.state.url}) - } - +export default class FeedSourceSettings extends Component { render () { const { activeComponent, activeSubComponent, - confirmDeleteFeedSource, updateExternalFeedResource, feedSource, project, user } = this.props - const { - name, - url - } = this.state const disabled = user.permissions && !user.permissions.hasFeedPermission( project.organizationId, project.id, feedSource.id, 'manage-feed' ) const isProjectAdmin = user.permissions && user.permissions.isProjectAdmin( project.id, project.organizationId ) - // const editGtfsDisabled = !user.permissions.hasFeedPermission(project.organizationId, project.id, feedSource.id, 'edit-gtfs') - const autoFetchFeed = feedSource.retrievalMethod === 'FETCHED_AUTOMATICALLY' const resourceType = activeComponent === 'settings' && activeSubComponent && activeSubComponent.toUpperCase() + const showTransformationsTab = resourceType === 'TRANSFORMATIONS' if (disabled) { return ( @@ -122,6 +68,11 @@ export default class FeedSourceSettings extends Component { active={!activeSubComponent}> General + + Feed Transformations + {Object.keys(feedSource.externalProperties || {}).map(resourceType => { const resourceLowerCase = resourceType.toLowerCase() return ( @@ -138,118 +89,26 @@ export default class FeedSourceSettings extends Component { {!resourceType - ? - {/* Settings */} - Settings}> - - - - Feed source name - - - - - - - - - - - - Make feed source deployable - - Enable this feed source to be deployed to an OpenTripPlanner (OTP) instance (defined in organization settings) as part of a collection of feed sources or individually. - - - - - Automatic fetch}> - - - - Feed source fetch URL - - - - - - - - - - - - Auto fetch feed source - - Set this feed source to fetch automatically. (Feed source URL must be specified and project auto fetch must be enabled.) - - - - - Danger zone}> - - - -

Make this feed source {feedSource.isPublic ? 'private' : 'public'}.

-

This feed source is currently {feedSource.isPublic ? 'public' : 'private'}.

-
- - -

Delete this feed source.

-

Once you delete a feed source, it cannot be recovered.

-
-
-
- - : - - + ? + : showTransformationsTab + // FIXME: Remove props spread. + ? + : + + }
) diff --git a/lib/manager/components/FeedTransformationSettings.js b/lib/manager/components/FeedTransformationSettings.js new file mode 100644 index 000000000..451c4246d --- /dev/null +++ b/lib/manager/components/FeedTransformationSettings.js @@ -0,0 +1,423 @@ +// @flow + +import Icon from '@conveyal/woonerf/components/icon' +import React, {Component} from 'react' +import { + Button, + ButtonToolbar, + Checkbox, + Col, + DropdownButton, + ListGroup, + ListGroupItem, + MenuItem, + Panel +} from 'react-bootstrap' +import Select from 'react-select' + +import {RETRIEVAL_METHODS} from '../../common/constants' +import {getGtfsSpec, getGtfsPlusSpec, isModuleEnabled} from '../../common/util/config' +import toSentenceCase from '../../common/util/to-sentence-case' +import VersionRetrievalBadge from './version/VersionRetrievalBadge' +import VersionSelectorDropdown from './version/VersionSelectorDropdown' + +// import type {Feed, Project} from '../../types' +// import type {ManagerUserState} from '../../types/reducers' + +function newRuleSet ( + retrievalMethods = ['FETCHED_AUTOMATICALLY', 'MANUALLY_UPLOADED'], + transformations = [] +) { + return { + retrievalMethods, + transformations + } +} + +/** + * Split camel case string by inserting white space between each word. + * @param {[type]} str [description] + * @return {[type]} [description] + */ +function splitCamelCase (str: string) { + // Regex finds/captures words in camel case string. + // Derived from: https://stackoverflow.com/a/18379358/915811 + return str.replace(/([a-z])([A-Z])/g, '$1 $2') +} + +export default class FeedTransformationSettings extends Component<*, *> { + _addRuleSet = () => { + const {feedSource, updateFeedSource} = this.props + const transformRules = [...feedSource.transformRules] + // If adding first rule set, use default retrieval methods. Otherwise, + // initialize to empty. + const ruleSet = transformRules.length === 0 + ? newRuleSet() + : newRuleSet([]) + transformRules.push(ruleSet) + updateFeedSource(feedSource, {transformRules}) + } + + _deleteRuleSet = (index: number) => { + const {feedSource, updateFeedSource} = this.props + const transformRules = [...feedSource.transformRules] + transformRules.splice(index, 1) + updateFeedSource(feedSource, {transformRules}) + } + + _saveRuleSet = (ruleSet: any, index: number) => { + const {feedSource, updateFeedSource} = this.props + const transformRules = [...feedSource.transformRules] + transformRules.splice(index, 1, ruleSet) + updateFeedSource(feedSource, {transformRules}) + } + + render () { + const { + disabled, + feedSource + } = this.props + // Do not allow users without manage-feed permission to modify feed + // transformation settings. + // TODO: Should we improve this to show the feed transformations, but disable + // making any changes? + if (disabled) { + return ( +

+ User is not authorized to modify feed transformation settings. +

+ ) + } + return ( + + {/* Settings */} + Transformation Settings}> + + +

+ Feed transformations provide a way to automatically transform + GTFS data that is loaded into Data Tools. Add a transformation, + describe when it should be applied (e.g., only to feeds uploaded + manually), and then define a series of steps to modify the data. +

+ +
+ {feedSource.transformRules.map((ruleSet, i) => { + return ( + + ) + })} +
+
+ + ) + } +} + +export function newFeedTransformation (type: string = 'ReplaceFileTransformation', props: any = {}) { + return { + '@type': type, + ...props + } +} + +const feedTransformationTypes = [ + 'ReplaceFileTransformation', + 'ReplaceFileFromStringTransformation' +] + +class FeedTransformRules extends Component<*, *> { + _addTransformation = (type: string) => { + const {index, onChange, ruleSet} = this.props + const transformations = [...ruleSet.transformations] + transformations.push(newFeedTransformation(type)) + onChange({...ruleSet, transformations}, index) + } + + _removeRuleSet = () => { + const {index, onDelete} = this.props + const ok = window.confirm( + `Are you sure you would like to delete Transformation ${index + 1}?` + ) + if (ok) onDelete(index) + } + + _onChangeTransformation = (changes, transformationIndex) => { + const {index, onChange, ruleSet} = this.props + const transformations = [...ruleSet.transformations] + transformations[transformationIndex] = {...transformations[transformationIndex], ...changes} + onChange({...ruleSet, transformations}, index) + } + + _onChangeRetrievalMethods = (options) => { + const {index, onChange, ruleSet} = this.props + const retrievalMethods = options.map(o => o.value) + onChange({...ruleSet, retrievalMethods}, index) + } + + _onToggleEnabled = (evt) => { + const {index, onChange, ruleSet} = this.props + onChange({...ruleSet, active: !ruleSet.active}, index) + } + + _removeTransformation = (transformationIndex: number) => { + const {index, onChange, ruleSet} = this.props + const transformations = [...ruleSet.transformations] + transformations.splice(transformationIndex, 1) + onChange({...ruleSet, transformations}, index) + } + + _retrievalMethodToOption = (method) => { + return { + value: method, + label: toSentenceCase(method.toLowerCase().split('_').join(' ')) + } + } + + render () { + const {feedSource, ruleSet, index} = this.props + const methodBadges = ruleSet.retrievalMethods.map(method => + ) + return ( + +

+ + + + + Transformation {index + 1}{' '} + {!ruleSet.active ? '(Paused) ' : ''} + {methodBadges} +

+ + Indicate which GTFS files this transformation applies to. + +