diff --git a/__tests__/test-utils/mock-data/manager.js b/__tests__/test-utils/mock-data/manager.js index c76fd0763..5d282687c 100644 --- a/__tests__/test-utils/mock-data/manager.js +++ b/__tests__/test-utils/mock-data/manager.js @@ -139,6 +139,7 @@ export const mockFeedWithVersion = { retrievalMethod: 'MANUALLY_UPLOADED', s3Url: null, snapshotVersion: null, + transformRules: [], url: 'http://mdtrip.org/googletransit/AnnapolisTransit/google_transit.zip', user: null } @@ -160,6 +161,7 @@ export const mockFeedWithoutVersion = { retrievalMethod: 'FETCHED_AUTOMATICALLY', s3Url: null, snapshotVersion: null, + transformRules: [], url: null, user: null } 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/i18n/english.yml b/i18n/english.yml index 60e3c00dc..5d8a0e331 100644 --- a/i18n/english.yml +++ b/i18n/english.yml @@ -228,6 +228,23 @@ components: upload: Upload versions: Versions viewPublic: View public page + FeedTransformationDescriptions: + general: + fileDefined: below text + filePlaceholder: '[choose file]' + tablePlaceholder: '[choose table]' + table: table + version: version + versionPlaceholder: '[choose version]' + DeleteRecordsTransformation: + label: Delete records from %tablePlaceholder% + name: Delete records transformation + ReplaceFileFromStringTransformation: + label: Replace %tablePlaceholder% from %filePlaceholder% + name: Replace file from string transformation + ReplaceFileFromVersionTransformation: + label: Replace %tablePlaceholder% from %versionPlaceholder% + name: Replace file from version transformation FeedVersionNavigator: confirmDelete: Are you sure you want to delete this version? This cannot be undone. confirmLoad: 'This will override all active GTFS Editor data for this Feed Source with the data from this version. If there is unsaved work in the Editor you want to keep, you must snapshot the current Editor data first. Are you sure you want to continue?' 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/common/constants/index.js b/lib/common/constants/index.js index cb78e6f29..e862c9363 100644 --- a/lib/common/constants/index.js +++ b/lib/common/constants/index.js @@ -1,4 +1,5 @@ // @flow + const SECURE: string = 'secure/' export const API_PREFIX: string = `/api/manager/` export const SECURE_API_PREFIX: string = `${API_PREFIX}${SECURE}` @@ -9,3 +10,18 @@ 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 = Object.freeze({ + MANUALLY_UPLOADED: 'MANUALLY_UPLOADED', + FETCHED_AUTOMATICALLY: 'FETCHED_AUTOMATICALLY', + PRODUCED_IN_HOUSE: 'PRODUCED_IN_HOUSE', + SERVICE_PERIOD_MERGE: 'SERVICE_PERIOD_MERGE', + REGIONAL_MERGE: 'REGIONAL_MERGE', + VERSION_CLONE: 'VERSION_CLONE' +}) + +export const FEED_TRANSFORMATION_TYPES = Object.freeze({ + DELETE_RECORDS: 'DeleteRecordsTransformation', + REPLACE_FILE_FROM_VERSION: 'ReplaceFileFromVersionTransformation', + REPLACE_FILE_FROM_STRING: 'ReplaceFileFromStringTransformation' +}) diff --git a/lib/common/util/config.js b/lib/common/util/config.js index 5c94779c6..e0b8ee794 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)) } /** @@ -58,7 +58,7 @@ export function getComponentMessages ( return message } else { if (logWarning) console.warn(`Couldn't find message entry for ${componentName}.${path}`) - return `{${path}}` + return message || `{${path}}` } } } 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/gtfsplus/components/GtfsPlusVersionSummary.js b/lib/gtfsplus/components/GtfsPlusVersionSummary.js index b794f0345..460bfe156 100644 --- a/lib/gtfsplus/components/GtfsPlusVersionSummary.js +++ b/lib/gtfsplus/components/GtfsPlusVersionSummary.js @@ -1,14 +1,24 @@ // @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, + 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 {GtfsPlusValidationIssue} from '../../types' import type {GtfsPlusReducerState, ManagerUserState} from '../../types/reducers' type Props = ContainerProps & { @@ -51,7 +61,7 @@ export default class GtfsPlusVersionSummary extends Component { return issuesForTable[tableId].length.toLocaleString() } - _getTableLevelIssues = (tableId: string) => { + _getTableLevelIssues = (tableId: string): ?Array => { const {issuesForTable} = this.props if (!issuesForTable) return null if (!(tableId in issuesForTable)) return null @@ -214,7 +224,7 @@ export default class GtfsPlusVersionSummary extends Component { {tableLevelIssues.length} critical table issue(s):
    {tableLevelIssues.map((issue, i) => -
  • {issue.description}
  • )} +
  • {issue.fieldName}: {issue.description}
  • )}
: null diff --git a/lib/manager/actions/__tests__/__snapshots__/projects.js.snap b/lib/manager/actions/__tests__/__snapshots__/projects.js.snap index c97363a74..6cee153b7 100644 --- a/lib/manager/actions/__tests__/__snapshots__/projects.js.snap +++ b/lib/manager/actions/__tests__/__snapshots__/projects.js.snap @@ -67,6 +67,7 @@ Object { "retrievalMethod": "MANUALLY_UPLOADED", "s3Url": null, "snapshotVersion": null, + "transformRules": Array [], "url": "http://mdtrip.org/googletransit/AnnapolisTransit/google_transit.zip", "user": null, }, @@ -162,6 +163,7 @@ Object { "retrievalMethod": "MANUALLY_UPLOADED", "s3Url": null, "snapshotVersion": null, + "transformRules": Array [], "url": "http://mdtrip.org/googletransit/AnnapolisTransit/google_transit.zip", "user": null, }, diff --git a/lib/manager/actions/status.js b/lib/manager/actions/status.js index f056fc7a7..e53eec23e 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 { 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,48 @@ 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] + // Increment count of skipped records for each value found per table. + skippedRecordsForTables[table] = (skippedRecordsForTables[table] || 0) + 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 +247,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 af8b1b902..d5dbf8359 100644 --- a/lib/manager/actions/versions.js +++ b/lib/manager/actions/versions.js @@ -164,7 +164,7 @@ export function publishFeedVersion (feedVersion: FeedVersion) { /** * 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')) @@ -290,11 +290,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/FeedSourceSettings.js b/lib/manager/components/FeedSourceSettings.js index a8d95bb92..d00fcb6b8 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 './transform/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,34 @@ 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 + ? - + project={project} + updateFeedSource={this.props.updateFeedSource} + user={user} + /> + : + + }
) diff --git a/lib/manager/components/GeneralSettings.js b/lib/manager/components/GeneralSettings.js new file mode 100644 index 000000000..933513a6f --- /dev/null +++ b/lib/manager/components/GeneralSettings.js @@ -0,0 +1,202 @@ +// @flow + +import Icon from '@conveyal/woonerf/components/icon' +import React, {Component} from 'react' +import { + Col, + ListGroup, + ListGroupItem, + Button, + Panel, + FormControl, + InputGroup, + ControlLabel, + FormGroup, + Checkbox +} from 'react-bootstrap' + +import * as feedsActions from '../actions/feeds' + +import type {Feed, Project} from '../../types' +import type {ManagerUserState} from '../../types/reducers' + +type Props = { + confirmDeleteFeedSource: () => void, + disabled: ?boolean, + feedSource: Feed, + project: Project, + updateFeedSource: typeof feedsActions.updateFeedSource, + user: ManagerUserState +} + +type State = { + name?: ?string, + url?: ?string +} + +export default class GeneralSettings 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}) + } + + render () { + const { + confirmDeleteFeedSource, + disabled, + feedSource + } = this.props + const { + name, + url + } = this.state + const autoFetchFeed = feedSource.retrievalMethod === 'FETCHED_AUTOMATICALLY' + return ( + + {/* 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.

+
+
+
+ + ) + } +} diff --git a/lib/manager/components/transform/FeedTransformRules.js b/lib/manager/components/transform/FeedTransformRules.js new file mode 100644 index 000000000..180cdfbee --- /dev/null +++ b/lib/manager/components/transform/FeedTransformRules.js @@ -0,0 +1,168 @@ +// @flow + +import Icon from '@conveyal/woonerf/components/icon' +import React, {Component} from 'react' +import { + Button, + ButtonToolbar, + DropdownButton, + ListGroupItem, + MenuItem +} from 'react-bootstrap' +import Select from 'react-select' + +import {RETRIEVAL_METHODS} from '../../../common/constants' +import toSentenceCase from '../../../common/util/to-sentence-case' +import FeedTransformation from './FeedTransformation' +import {getTransformationName} from '../../util/transform' +import VersionRetrievalBadge from '../version/VersionRetrievalBadge' + +import type { + Feed, + FeedTransformRules as FeedTransformRulesType, + ReactSelectOption +} from '../../../types' + +function newFeedTransformation (type: string = 'ReplaceFileFromVersionTransformation', props: any = {}) { + return { + '@type': type, + ...props + } +} + +const feedTransformationTypes = [ + 'ReplaceFileFromVersionTransformation', + 'ReplaceFileFromStringTransformation' +] + +type TransformRulesProps = { + feedSource: Feed, + index: number, + onChange: (any, number) => void, + onDelete: (number) => void, + ruleSet: FeedTransformRulesType +} +/** + * Component that shows a single set of transform rules, which correspond to a + * specific set of retrieval methods. This can contain contain multiple + * transformations that will apply in sequence to an incoming GTFS file. + */ +export default 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 deleteConfirmed = window.confirm( + `Are you sure you would like to delete Transformation ${index + 1}?` + ) + if (deleteConfirmed) onDelete(index) + } + + _onChangeTransformation = (changes: any, transformationIndex: number) => { + const {index, onChange, ruleSet} = this.props + const transformations = [...ruleSet.transformations] + transformations[transformationIndex] = {...transformations[transformationIndex], ...changes} + onChange({...ruleSet, transformations}, index) + } + + _onChangeRetrievalMethods = (options: Array) => { + const {index, onChange, ruleSet} = this.props + const retrievalMethods = options.map(o => o.value) + onChange({...ruleSet, retrievalMethods}, index) + } + + _onToggleEnabled = (evt: SyntheticInputEvent) => { + 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: mixed) => { + // Annoying check because our version of flow appears to treat Object.values + // as Array: https://github.com/facebook/flow/issues/2221#issuecomment-238749128 + if (typeof method === 'string') { + return { + value: method, + label: toSentenceCase(method.toLowerCase().split('_').join(' ')) + } + } + return null + } + + render () { + const {feedSource, ruleSet, index} = this.props + const methodBadges = ruleSet.retrievalMethods.map(method => + ) + return ( + +

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

+ + Apply this transformation to GTFS feeds created through the following methods. + +