Skip to content

Commit 5fb9ec9

Browse files
feat(GtfsPlusVersionSummary): Add GTFS+ validation issue details.
1 parent a106298 commit 5fb9ec9

File tree

1 file changed

+144
-42
lines changed

1 file changed

+144
-42
lines changed

lib/gtfsplus/components/GtfsPlusVersionSummary.js

Lines changed: 144 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,33 @@
11
// @flow
22

3+
import moment from 'moment'
34
import 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'
516
import {browserHistory, Link} from 'react-router'
6-
import moment from 'moment'
717

8-
import {getGtfsPlusSpec} from '../../common/util/config'
918
import * as gtfsPlusActions from '../actions/gtfsplus'
10-
19+
import {getGtfsPlusSpec} from '../../common/util/config'
1120
import type {Props as ContainerProps} from '../containers/ActiveGtfsPlusVersionSummary'
21+
import type {GtfsSpecTable} from '../../types'
1222
import type {GtfsPlusReducerState, ManagerUserState} from '../../types/reducers'
1323

24+
type Issue = {
25+
description: string,
26+
fieldName: string,
27+
rowIndex: number,
28+
tableId: string
29+
}
30+
1431
type Props = ContainerProps & {
1532
deleteGtfsPlusFeed: typeof gtfsPlusActions.deleteGtfsPlusFeed,
1633
downloadGtfsPlusFeed: typeof gtfsPlusActions.downloadGtfsPlusFeed,
@@ -21,11 +38,17 @@ type Props = ContainerProps & {
2138
}
2239

2340
type State = {
24-
expanded: boolean
41+
expanded: boolean,
42+
tableExpanded: any
2543
}
2644

45+
type IssueFilter = Issue => boolean
46+
2747
export 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

Comments
 (0)