11// @flow
22
3+ import moment from 'moment'
34import React , { Component } from 'react'
4- import { Panel , Row , Col , Table , Button , ButtonToolbar , Glyphicon , Alert } from 'react-bootstrap'
5+ import {
6+ Alert ,
7+ Button ,
8+ ButtonToolbar ,
9+ Col ,
10+ Glyphicon ,
11+ Label ,
12+ Panel ,
13+ Row ,
14+ Table
15+ } from 'react-bootstrap'
516import { browserHistory , Link } from 'react-router'
6- import moment from 'moment'
717
8- import { getGtfsPlusSpec } from '../../common/util/config'
918import * as gtfsPlusActions from '../actions/gtfsplus'
10-
19+ import { getGtfsPlusSpec } from '../../common/util/config'
1120import type { Props as ContainerProps } from '../containers/ActiveGtfsPlusVersionSummary'
21+ import type { GtfsSpecTable } from '../../types'
1222import type { GtfsPlusReducerState , ManagerUserState } from '../../types/reducers'
1323
24+ type Issue = {
25+ description : string ,
26+ fieldName : string ,
27+ rowIndex : number ,
28+ tableId : string
29+ }
30+
1431type Props = ContainerProps & {
1532 deleteGtfsPlusFeed : typeof gtfsPlusActions . deleteGtfsPlusFeed ,
1633 downloadGtfsPlusFeed : typeof gtfsPlusActions . downloadGtfsPlusFeed ,
@@ -21,11 +38,17 @@ type Props = ContainerProps & {
2138}
2239
2340type State = {
24- expanded : boolean
41+ expanded : boolean ,
42+ tableExpanded : any
2543}
2644
45+ type IssueFilter = Issue => boolean
46+
2747export default class GtfsPlusVersionSummary extends Component < Props , State > {
28- state = { expanded : false }
48+ state = {
49+ expanded : false ,
50+ tableExpanded : { }
51+ }
2952
3053 componentDidMount ( ) {
3154 this . props . downloadGtfsPlusFeed ( this . props . version . id )
@@ -51,13 +74,19 @@ export default class GtfsPlusVersionSummary extends Component<Props, State> {
5174 return issuesForTable [ tableId ] . length . toLocaleString ( )
5275 }
5376
54- _getTableLevelIssues = ( tableId : string ) => {
77+ _getTableLevelIssues = ( tableId : string ) : ?Array < Issue > => {
78+ return this . _getIssues ( tableId , issue => issue . rowIndex === - 1 )
79+ }
80+
81+ _getIssues = ( tableId : string , filter : ?IssueFilter ) : ?Array < Issue > => {
5582 const { issuesForTable} = this . props
5683 if ( ! issuesForTable ) return null
5784 if ( ! ( tableId in issuesForTable ) ) return null
58- // Table level issues are identified by not having -1 for row index.
59- const tableLevelIssues = issuesForTable [ tableId ] . filter ( issue => issue . rowIndex === - 1 )
60- return tableLevelIssues . length > 0 ? tableLevelIssues : null
85+
86+ // Filter table level issues or row issues using the specified filter.
87+ filter = filter || ( ( ) => true )
88+ const issues = issuesForTable [ tableId ] . filter ( filter )
89+ return ( issues . length > 0 ? issues : null )
6190 }
6291
6392 feedStatus ( ) {
@@ -95,6 +124,68 @@ export default class GtfsPlusVersionSummary extends Component<Props, State> {
95124 this . setState ( { expanded : ! expanded } )
96125 }
97126
127+ _toggleTableExpanded = ( tableName : string ) : void => {
128+ const { tableExpanded } = this . state
129+ const newTableExpanded = Object . assign ( tableExpanded )
130+ newTableExpanded [ tableName ] = ! newTableExpanded [ tableName ]
131+
132+ this . setState ( { tableExpanded : newTableExpanded } )
133+ }
134+
135+ renderIssues = ( table : GtfsSpecTable ) => {
136+ const { tableExpanded } = this . state
137+ const isExpanded = tableExpanded [ table . name ]
138+ const issueCount = this . validationIssueCount ( table . id )
139+ const tableLevelIssues = this . _getTableLevelIssues ( table . id )
140+ const allIssues = this . _getIssues ( table . id )
141+ allIssues && allIssues . sort (
142+ ( issue1 , issue2 ) => issue1 . rowIndex - issue2 . rowIndex
143+ )
144+
145+ return (
146+ < div >
147+ < small >
148+ < Button
149+ bsSize = 'small'
150+ bsStyle = 'link'
151+ onClick = { ( ) => this . _toggleTableExpanded ( table . name ) }
152+ >
153+ < small >
154+ < Glyphicon
155+ glyph = { isExpanded ? 'triangle-bottom' : 'triangle-right' }
156+ style = { { marginRight : '0.5em' } } />
157+ </ small >
158+ { issueCount } validation { issueCount !== 1 ? 'issues' : 'issue' }
159+ { tableLevelIssues && issueCount && ` (${ tableLevelIssues . length } /${ issueCount } blocking)` }
160+ </ Button >
161+
162+ { isExpanded && < Table condensed >
163+ < thead >
164+ < tr >
165+ < th > Line</ th >
166+ < th > Column</ th >
167+ < th > Issue</ th >
168+ </ tr >
169+ </ thead >
170+ < tbody >
171+ { allIssues && allIssues . map ( ( issue , index ) =>
172+ < tr key = { index } >
173+ < td > { issue . rowIndex + 2 } </ td > { /* This is the line number in the file */ }
174+ < td > { issue . fieldName } </ td >
175+ < td >
176+ { issue . description }
177+ { ' ' }
178+ { issue . rowIndex === - 1 && < Label bsStyle = 'danger' htmlFor > BLOCKING</ Label > }
179+ </ td >
180+ </ tr >
181+ ) }
182+ </ tbody >
183+ </ Table > }
184+ </ small >
185+ </ div >
186+ )
187+ }
188+
98189 render ( ) {
99190 const {
100191 gtfsplus,
@@ -183,51 +274,62 @@ export default class GtfsPlusVersionSummary extends Component<Props, State> {
183274 < Row >
184275 < Col xs = { 12 } >
185276 < Panel >
186- < Table striped fill >
277+ < Table fill >
187278 < thead >
188279 < tr >
189280 < th > Table</ th >
190281 < th > Included?</ th >
191282 < th > Records</ th >
192283 < th > Validation Issues</ th >
193- < th />
194284 </ tr >
195285 </ thead >
196- < tbody >
197- { getGtfsPlusSpec ( ) . map ( ( table , index ) => {
286+ { /* FIXME: reinstate this <tbody> after switching to React 16. */ }
287+ { /**
288+ * Change the behavior as follows:
289+ * - Table-level issues are still critical and blocking and and displayed in red.
290+ * - Per-row issues are still amber warnings and non-blocking,
291+ * but will now be displayed individually instead of being aggregated.
292+ * Maybe only display the first 25 issues to avoid long rendering times???
293+ * - Issues are displayed on a full-width sub-table for better readability,
294+ * in the same "row" as the issue summary.
295+ * - Tables are sorted alphabetically.
296+ */ }
297+ { getGtfsPlusSpec ( )
298+ . sort ( ( table1 , table2 ) => table1 . name . localeCompare ( table2 . name ) )
299+ . map ( ( table , index ) => {
198300 const issueCount = this . validationIssueCount ( table . id )
199301 const tableLevelIssues = this . _getTableLevelIssues ( table . id )
302+ const hasIssues = + issueCount > 0
303+ const className = tableLevelIssues
304+ ? 'danger'
305+ : ( hasIssues ? 'warning' : '' )
306+
200307 return (
201- < tr
202- rowSpan = { tableLevelIssues ? 2 : 1 }
203- key = { index }
204- className = { tableLevelIssues
205- ? 'danger'
206- : + issueCount > 0 && 'warning'
207- }
208- style = { { color : this . isTableIncluded ( table . id ) === 'Yes' ? 'black' : 'lightGray' } } >
209- < td >
210- { table . name }
211- { tableLevelIssues
212- ? < small >
213- < br />
214- { tableLevelIssues . length } critical table issue(s):
215- < ul >
216- { tableLevelIssues . map ( ( issue , i ) =>
217- < li key = { i } > { issue . description } </ li > ) }
218- </ ul >
219- </ small >
220- : null
221- }
222- </ td >
223- < td > { this . isTableIncluded ( table . id ) } </ td >
224- < td > { this . tableRecordCount ( table . id ) } </ td >
225- < td > { issueCount } </ td >
226- < td />
227- </ tr >
308+ // FIXME: Use <React.Fragment key={index}> (React 16+ only.)
309+ < tbody key = { index } >
310+ < tr
311+ className = { className }
312+ rowSpan = { hasIssues ? 2 : 1 }
313+ style = { { color : this . isTableIncluded ( table . id ) === 'Yes' ? 'black' : 'lightGray' } } >
314+ < td >
315+ { table . name }
316+ </ td >
317+ < td > { this . isTableIncluded ( table . id ) } </ td >
318+ < td > { this . tableRecordCount ( table . id ) } </ td >
319+ < td > { issueCount } </ td >
320+ </ tr >
321+ { hasIssues && (
322+ < tr className = { className } >
323+ < td colSpan = '4' >
324+ { this . renderIssues ( table ) }
325+ </ td >
326+ </ tr >
327+ ) }
328+ </ tbody >
329+ // </React.Fragment>
228330 )
229331 } ) }
230- </ tbody >
332+ { /* </tbody> */ }
231333 </ Table >
232334 </ Panel >
233335 </ Col >
0 commit comments