phobos_checkpoint_ui 0.4.0 → 1.0.0.rc1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +5 -1
  3. data/frontend/.gitignore +4 -0
  4. data/frontend/src/actions/events-search.js +11 -11
  5. data/frontend/src/actions/events-search.spec.js +21 -21
  6. data/frontend/src/actions/failures/details.js +42 -0
  7. data/frontend/src/actions/failures/details.spec.js +71 -0
  8. data/frontend/src/actions/failures/overview.js +14 -0
  9. data/frontend/src/actions/failures/overview.spec.js +20 -0
  10. data/frontend/src/actions/failures/retry.js +58 -0
  11. data/frontend/src/actions/failures/retry.spec.js +117 -0
  12. data/frontend/src/actions/failures/search/index.js +76 -0
  13. data/frontend/src/actions/failures/search/index.spec.js +197 -0
  14. data/frontend/src/actions/index.js +24 -5
  15. data/frontend/src/actions/navigation/index.js +5 -0
  16. data/frontend/src/actions/navigation/index.spec.js +21 -0
  17. data/frontend/src/api.js +11 -0
  18. data/frontend/src/components/{event-overview/attribute.js → attribute/index.js} +0 -0
  19. data/frontend/src/components/empty-event/index.js +20 -0
  20. data/frontend/src/components/empty-event/index.spec.js +38 -0
  21. data/frontend/src/components/event-overview/index.js +1 -1
  22. data/frontend/src/components/event-retry-dialog/index.js +1 -1
  23. data/frontend/src/components/failure/error-message.js +19 -0
  24. data/frontend/src/components/failure/error-message.scss +8 -0
  25. data/frontend/src/components/failure/event.scss +3 -0
  26. data/frontend/src/components/failure/index.js +82 -0
  27. data/frontend/src/components/failure/index.spec.js +89 -0
  28. data/frontend/src/components/failure/loading.js +16 -0
  29. data/frontend/src/components/failure/overview/failure-overview.scss +28 -0
  30. data/frontend/src/components/failure/overview/index.js +60 -0
  31. data/frontend/src/components/failure/overview/index.spec.js +71 -0
  32. data/frontend/src/components/failure/overview-dialog/event-overview-dialog.scss +15 -0
  33. data/frontend/src/components/failure/overview-dialog/index.js +90 -0
  34. data/frontend/src/components/failure/retry-dialog/index.js +72 -0
  35. data/frontend/src/components/failure/style.js +16 -0
  36. data/frontend/src/components/failures-list/failures-list.scss +49 -0
  37. data/frontend/src/components/failures-list/index.js +62 -0
  38. data/frontend/src/components/failures-list/index.spec.js +59 -0
  39. data/frontend/src/components/header/index.js +36 -2
  40. data/frontend/src/components/load-more/index.js +22 -0
  41. data/frontend/src/components/load-more/index.spec.js +54 -0
  42. data/frontend/src/components/search-input/index.js +2 -5
  43. data/frontend/src/components/search-input/index.spec.js +6 -6
  44. data/frontend/src/reducers/events.js +2 -2
  45. data/frontend/src/reducers/events.spec.js +4 -4
  46. data/frontend/src/reducers/failures/details/index.js +32 -0
  47. data/frontend/src/reducers/failures/details/index.spec.js +54 -0
  48. data/frontend/src/reducers/failures/index.js +48 -0
  49. data/frontend/src/reducers/failures/index.spec.js +95 -0
  50. data/frontend/src/reducers/index.js +4 -1
  51. data/frontend/src/reducers/index.spec.js +2 -0
  52. data/frontend/src/reducers/xhr-status.js +25 -10
  53. data/frontend/src/reducers/xhr-status.spec.js +65 -15
  54. data/frontend/src/routes.js +6 -2
  55. data/frontend/src/store.js +7 -3
  56. data/frontend/src/views/{event-details.js → events/details/index.js} +0 -0
  57. data/frontend/src/views/{events-search.js → events/search/index.js} +37 -44
  58. data/frontend/src/views/{events-search.scss → events/search/index.scss} +0 -0
  59. data/frontend/src/views/{events-search.spec.js → events/search/index.spec.js} +35 -25
  60. data/frontend/src/views/failures/details/index.js +50 -0
  61. data/frontend/src/views/failures/search/index.js +113 -0
  62. data/frontend/src/views/failures/search/index.scss +24 -0
  63. data/frontend/src/views/failures/search/index.spec.js +106 -0
  64. data/lib/phobos_checkpoint_ui/version.rb +1 -1
  65. data/phobos_checkpoint_ui.gemspec +1 -1
  66. metadata +53 -14
@@ -0,0 +1,197 @@
1
+ import 'babel-polyfill'
2
+ import Mappersmith from 'mappersmith'
3
+ import 'mappersmith/fixtures'
4
+ import configureMockStore from 'redux-mock-store'
5
+ import thunk from 'redux-thunk'
6
+ import { EVENTS_SEARCH_LIMIT } from 'api'
7
+
8
+ const middlewares = [ thunk ]
9
+ const mockStore = configureMockStore(middlewares)
10
+
11
+ import {
12
+ TRIGGER_FAILURES_SEARCH,
13
+ REQUEST_FAILURES_SEARCH_RESULTS,
14
+ RECEIVE_FAILURES_SEARCH_RESULTS,
15
+ REQUEST_FAILURES_SEARCH_RESULTS_FAILED,
16
+ ADD_FLASH_MESSAGE,
17
+ LOAD_MORE_FAILURES_SEARCH_RESULTS
18
+ } from 'actions'
19
+
20
+ import {
21
+ triggerSearch,
22
+ fetchSearchResults,
23
+ loadMoreSearchResults
24
+ } from 'actions/failures/search'
25
+
26
+ beforeEach(() => {
27
+ Mappersmith.Env.Fixture.clear()
28
+ })
29
+
30
+ describe('actions/failures/search', () => {
31
+ describe('#fetchSearchResults', () => {
32
+ describe('without filters', () => {
33
+ let failure, initialState, store
34
+ beforeEach(() => {
35
+ initialState = { eventsFilters: {}, xhrStatus: { currentEventsOffset: 0 } }
36
+ store = mockStore(initialState)
37
+ failure = { id: 1 }
38
+ Mappersmith.Env.Fixture
39
+ .define('get')
40
+ .matching({ url: `/api/v1/failures?limit=${EVENTS_SEARCH_LIMIT}&offset=0` })
41
+ .response([failure])
42
+ })
43
+
44
+ it('creates REQUEST and RECEIVE actions', (done) => {
45
+ store.dispatch(fetchSearchResults(failure)).then(() => {
46
+ const actions = store.getActions()
47
+ expect(actions[0]).toEqual({ type: REQUEST_FAILURES_SEARCH_RESULTS })
48
+ expect(actions[1]).toEqual({ type: RECEIVE_FAILURES_SEARCH_RESULTS, failures: [failure], offset: 0 })
49
+ done()
50
+ })
51
+ .catch((e) => done.fail(`test failed with promise error: ${e.message}`))
52
+ })
53
+ })
54
+
55
+ describe('with specific filters', () => {
56
+ let failure, initialState, store
57
+ beforeEach(() => {
58
+ initialState = {
59
+ eventsFilters: { type: 'event_type', value: 'new' },
60
+ xhrStatus: { currentEventsOffset: 0 }
61
+ }
62
+ store = mockStore(initialState)
63
+ failure = { id: 1 }
64
+ Mappersmith.Env.Fixture
65
+ .define('get')
66
+ .matching({ url: `/api/v1/failures?limit=${EVENTS_SEARCH_LIMIT}&event_type=new&offset=0` })
67
+ .response([failure])
68
+ })
69
+
70
+ it('creates REQUEST and RECEIVE actions using the filters', (done) => {
71
+ store.dispatch(fetchSearchResults(failure)).then(() => {
72
+ const actions = store.getActions()
73
+ expect(actions[0]).toEqual({ type: REQUEST_FAILURES_SEARCH_RESULTS })
74
+ expect(actions[1]).toEqual({ type: RECEIVE_FAILURES_SEARCH_RESULTS, failures: [failure], offset: 0 })
75
+ done()
76
+ })
77
+ .catch((e) => done.fail(`test failed with promise error: ${e.message}`))
78
+ })
79
+ })
80
+
81
+ describe('with a different offset', () => {
82
+ let failure, initialState, store
83
+ beforeEach(() => {
84
+ initialState = {
85
+ eventsFilters: { },
86
+ xhrStatus: { currentEventsOffset: 4 }
87
+ }
88
+ store = mockStore(initialState)
89
+ failure = { id: 1 }
90
+ Mappersmith.Env.Fixture
91
+ .define('get')
92
+ .matching({ url: `/api/v1/failures?limit=${EVENTS_SEARCH_LIMIT}&offset=4` })
93
+ .response([failure])
94
+ })
95
+
96
+ it('creates REQUEST and RECEIVE actions pointing to the correct offset', (done) => {
97
+ store.dispatch(fetchSearchResults(failure)).then(() => {
98
+ const actions = store.getActions()
99
+ expect(actions[0]).toEqual({ type: REQUEST_FAILURES_SEARCH_RESULTS })
100
+ expect(actions[1]).toEqual({ type: RECEIVE_FAILURES_SEARCH_RESULTS, failures: [failure], offset: 4 })
101
+ done()
102
+ })
103
+ .catch((e) => done.fail(`test failed with promise error: ${e.message}`))
104
+ })
105
+ })
106
+
107
+ describe('when it fails', () => {
108
+ let failure, initialState, store
109
+ beforeEach(() => {
110
+ initialState = { eventsFilters: {}, xhrStatus: { currentEventsOffset: 0 } }
111
+ store = mockStore(initialState)
112
+ failure = { id: 1 }
113
+ Mappersmith.Env.Fixture
114
+ .define('get')
115
+ .matching({ url: `/api/v1/failures?limit=${EVENTS_SEARCH_LIMIT}&offset=0` })
116
+ .failure()
117
+ .response({
118
+ responseText: JSON.stringify({
119
+ error: true,
120
+ message: 'some error'
121
+ })
122
+ })
123
+ })
124
+
125
+ it('creates REQUEST and RECEIVE actions pointing to the correct offset', (done) => {
126
+ store.dispatch(fetchSearchResults(failure)).then(() => {
127
+ const actions = store.getActions()
128
+ expect(actions[0]).toEqual({ type: REQUEST_FAILURES_SEARCH_RESULTS })
129
+ expect(actions[1]).toEqual({
130
+ type: REQUEST_FAILURES_SEARCH_RESULTS_FAILED,
131
+ query: { offset: 0 }, error: 'some error'
132
+ })
133
+ expect(actions[2]).toEqual({
134
+ type: ADD_FLASH_MESSAGE,
135
+ message: { id: jasmine.any(String), type: 'error', text: 'Failures search failed. "some error"' }
136
+ })
137
+ done()
138
+ })
139
+ .catch((e) => done.fail(`test failed with promise error: ${e.message}`))
140
+ })
141
+ })
142
+ })
143
+
144
+ describe('#triggerSearch', () => {
145
+ let failure, initialState, store
146
+ beforeEach(() => {
147
+ initialState = {
148
+ eventsFilters: {},
149
+ xhrStatus: { currentEventsOffset: 0 }
150
+ }
151
+ store = mockStore(initialState)
152
+ failure = { id: 1 }
153
+ Mappersmith.Env.Fixture
154
+ .define('get')
155
+ .matching({ url: `/api/v1/failures?limit=${EVENTS_SEARCH_LIMIT}&offset=0` })
156
+ .response([failure])
157
+ })
158
+
159
+ it('creates TRIGGER_FAILURES_SEARCH and REQUEST actions', (done) => {
160
+ store.dispatch(triggerSearch()).then(() => {
161
+ const actions = store.getActions()
162
+ expect(actions[0]).toEqual({ type: TRIGGER_FAILURES_SEARCH })
163
+ expect(actions[1]).toEqual({ type: REQUEST_FAILURES_SEARCH_RESULTS })
164
+ expect(actions[2]).toEqual({ type: RECEIVE_FAILURES_SEARCH_RESULTS, failures: [failure], offset: 0 })
165
+ done()
166
+ })
167
+ .catch((e) => done.fail(`test failed with promise error: ${e.message}`))
168
+ })
169
+ })
170
+
171
+ describe('#loadMoreSearchResults', () => {
172
+ let failure, initialState, store
173
+ beforeEach(() => {
174
+ initialState = {
175
+ eventsFilters: {},
176
+ xhrStatus: { currentEventsOffset: 4 }
177
+ }
178
+ store = mockStore(initialState)
179
+ failure = { id: 1 }
180
+ Mappersmith.Env.Fixture
181
+ .define('get')
182
+ .matching({ url: `/api/v1/failures?limit=${EVENTS_SEARCH_LIMIT}&offset=4` })
183
+ .response([failure])
184
+ })
185
+
186
+ it('creates LOAD_MORE_FAILURES_SEARCH_RESULTS and REQUEST actions', (done) => {
187
+ store.dispatch(loadMoreSearchResults()).then(() => {
188
+ const actions = store.getActions()
189
+ expect(actions[0]).toEqual({ type: LOAD_MORE_FAILURES_SEARCH_RESULTS, offset: 4 + EVENTS_SEARCH_LIMIT })
190
+ expect(actions[1]).toEqual({ type: REQUEST_FAILURES_SEARCH_RESULTS })
191
+ expect(actions[2]).toEqual({ type: RECEIVE_FAILURES_SEARCH_RESULTS, failures: [failure], offset: 4 })
192
+ done()
193
+ })
194
+ .catch((e) => done.fail(`test failed with promise error: ${e.message}`))
195
+ })
196
+ })
197
+ })
@@ -1,20 +1,35 @@
1
1
  export const EVENT_SHOW_OVERVIEW = 'EVENT_SHOW_OVERVIEW'
2
2
  export const EVENT_HIDE_OVERVIEW = 'EVENT_HIDE_OVERVIEW'
3
3
 
4
+ export const FAILURE_SHOW_OVERVIEW = 'FAILURE_SHOW_OVERVIEW'
5
+ export const FAILURE_HIDE_OVERVIEW = 'FAILURE_HIDE_OVERVIEW'
6
+
4
7
  export const EVENT_SHOW_RETRY = 'EVENT_SHOW_RETRY'
5
8
  export const EVENT_HIDE_RETRY = 'EVENT_HIDE_RETRY'
6
9
  export const REQUEST_EVENT_RETRY = 'REQUEST_EVENT_RETRY'
7
10
  export const RECEIVE_EVENT_RETRY = 'RECEIVE_EVENT_RETRY'
8
11
  export const REQUEST_EVENT_RETRY_FAILED = 'REQUEST_EVENT_RETRY_FAILED'
9
12
 
13
+ export const FAILURE_SHOW_RETRY = 'FAILURE_SHOW_RETRY'
14
+ export const FAILURE_HIDE_RETRY = 'FAILURE_HIDE_RETRY'
15
+ export const REQUEST_FAILURE_RETRY = 'REQUEST_FAILURE_RETRY'
16
+ export const RECEIVE_FAILURE_RETRY = 'RECEIVE_FAILURE_RETRY'
17
+ export const REQUEST_FAILURE_RETRY_FAILED = 'REQUEST_FAILURE_RETRY_FAILED'
18
+
10
19
  export const SEARCH_INPUT_CHANGE_FILTER_TYPE = 'SEARCH_INPUT_CHANGE_FILTER_TYPE'
11
20
  export const SEARCH_INPUT_CHANGE_FILTER_VALUE = 'SEARCH_INPUT_CHANGE_FILTER_VALUE'
12
21
 
13
- export const REQUEST_SEARCH_RESULTS = 'REQUEST_SEARCH_RESULTS'
14
- export const RECEIVE_SEARCH_RESULTS = 'RECEIVE_SEARCH_RESULTS'
15
- export const REQUEST_SEARCH_RESULTS_FAILED = 'REQUEST_SEARCH_RESULTS_FAILED'
16
- export const LOAD_MORE_SEARCH_RESULTS = 'LOAD_MORE_SEARCH_RESULTS'
17
- export const TRIGGER_SEARCH = 'TRIGGER_SEARCH'
22
+ export const REQUEST_EVENTS_SEARCH_RESULTS = 'REQUEST_EVENTS_SEARCH_RESULTS'
23
+ export const RECEIVE_EVENTS_SEARCH_RESULTS = 'RECEIVE_EVENTS_SEARCH_RESULTS'
24
+ export const REQUEST_EVENTS_SEARCH_RESULTS_FAILED = 'REQUEST_EVENTS_SEARCH_RESULTS_FAILED'
25
+ export const LOAD_MORE_EVENTS_SEARCH_RESULTS = 'LOAD_MORE_EVENTS_SEARCH_RESULTS'
26
+ export const TRIGGER_EVENTS_SEARCH = 'TRIGGER_EVENTS_SEARCH'
27
+
28
+ export const REQUEST_FAILURES_SEARCH_RESULTS = 'REQUEST_FAILURES_SEARCH_RESULTS'
29
+ export const RECEIVE_FAILURES_SEARCH_RESULTS = 'RECEIVE_FAILURES_SEARCH_RESULTS'
30
+ export const REQUEST_FAILURES_SEARCH_RESULTS_FAILED = 'REQUEST_FAILURES_SEARCH_RESULTS_FAILED'
31
+ export const LOAD_MORE_FAILURES_SEARCH_RESULTS = 'LOAD_MORE_FAILURES_SEARCH_RESULTS'
32
+ export const TRIGGER_FAILURES_SEARCH = 'TRIGGER_FAILURES_SEARCH'
18
33
 
19
34
  export const ADD_FLASH_MESSAGE = 'ADD_FLASH_MESSAGE'
20
35
  export const DELETE_FLASH_MESSAGE = 'DELETE_FLASH_MESSAGE'
@@ -22,3 +37,7 @@ export const DELETE_FLASH_MESSAGE = 'DELETE_FLASH_MESSAGE'
22
37
  export const REQUEST_EVENT_DETAILS = 'REQUEST_EVENT_DETAILS'
23
38
  export const RECEIVE_EVENT_DETAILS = 'RECEIVE_EVENT_DETAILS'
24
39
  export const REQUEST_EVENT_DETAILS_FAILED = 'REQUEST_EVENT_DETAILS_FAILED'
40
+
41
+ export const REQUEST_FAILURE_DETAILS = 'REQUEST_FAILURE_DETAILS'
42
+ export const RECEIVE_FAILURE_DETAILS = 'RECEIVE_FAILURE_DETAILS'
43
+ export const REQUEST_FAILURE_DETAILS_FAILED = 'REQUEST_FAILURE_DETAILS_FAILED'
@@ -0,0 +1,5 @@
1
+ import { push } from 'react-router-redux'
2
+
3
+ export const navigateTo = (path) => (dispatch) => {
4
+ dispatch(push(path))
5
+ }
@@ -0,0 +1,21 @@
1
+ import 'babel-polyfill'
2
+ import configureMockStore from 'redux-mock-store'
3
+ import thunk from 'redux-thunk'
4
+ import { push } from 'react-router-redux'
5
+
6
+ import { navigateTo } from 'actions/navigation'
7
+
8
+ const mockStore = configureMockStore([thunk])
9
+
10
+ describe('action navigate', () => {
11
+ let store
12
+ beforeEach(() => {
13
+ store = mockStore({})
14
+ })
15
+
16
+ it('dispatches a push event from react-router-redux', () => {
17
+ store.dispatch(navigateTo('/foo'))
18
+ const actions = store.getActions()
19
+ expect(actions[0]).toEqual(push('/foo'))
20
+ })
21
+ })
data/frontend/src/api.js CHANGED
@@ -41,6 +41,17 @@ export default Mappersmith.forge({
41
41
  path: '/api/v1/events',
42
42
  params: { limit: EVENTS_SEARCH_LIMIT }
43
43
  }
44
+ },
45
+ Failure: {
46
+ findById: '/api/v1/failures/{id}',
47
+ retry: {
48
+ path: '/api/v1/failures/{id}/retry',
49
+ method: 'POST'
50
+ },
51
+ search: {
52
+ path: '/api/v1/failures',
53
+ params: { limit: EVENTS_SEARCH_LIMIT }
54
+ }
44
55
  }
45
56
  }
46
57
  })
@@ -0,0 +1,20 @@
1
+ import React, { Component, PropTypes } from 'react'
2
+
3
+ export default class EmptyEvent extends Component {
4
+ static get propTypes () {
5
+ return {
6
+ events: PropTypes.array.isRequired,
7
+ isFetchingEvents: PropTypes.bool.isRequired
8
+ }
9
+ }
10
+
11
+ render () {
12
+ return (
13
+ this.props.events.length === 0 &&
14
+ !this.props.isFetchingEvents &&
15
+ <div className='empty-event'>
16
+ No events found
17
+ </div>
18
+ )
19
+ }
20
+ }
@@ -0,0 +1,38 @@
1
+ import React from 'react'
2
+ import jasmineEnzyme from 'jasmine-enzyme'
3
+ import { shallow } from 'enzyme'
4
+
5
+ import EmptyEvent from 'components/empty-event'
6
+
7
+ describe('<EmptyEvent />', () => {
8
+ let props, wrapper
9
+
10
+ beforeEach(() => {
11
+ jasmineEnzyme()
12
+ props = {
13
+ events: [{ id: 1 }, { id: 2 }],
14
+ isFetchingEvents: false
15
+ }
16
+ wrapper = shallow(<EmptyEvent {...props} />)
17
+ })
18
+
19
+ describe('with events', () => {
20
+ it('does not render <EmptyEvent />', () => {
21
+ expect(wrapper.find('.empty-event').length).toEqual(0)
22
+ })
23
+ })
24
+
25
+ describe('without events', () => {
26
+ beforeEach(() => {
27
+ props = {
28
+ ...props,
29
+ events: []
30
+ }
31
+ wrapper = shallow(<EmptyEvent {...props} />)
32
+ })
33
+
34
+ it('renders <EmptyEvent />', () => {
35
+ expect(wrapper.find('.empty-event').length).toEqual(1)
36
+ })
37
+ })
38
+ })
@@ -3,7 +3,7 @@ import moment from 'moment'
3
3
  import JSONPretty from 'react-json-pretty'
4
4
  import 'react-json-pretty/src/JSONPretty.monikai.css'
5
5
 
6
- import Attribute from 'components/event-overview/attribute'
6
+ import Attribute from 'components/attribute'
7
7
 
8
8
  const EVENT_TIME_FORMAT = 'MMMM Do YYYY, h:mm:ss a'
9
9
 
@@ -18,7 +18,7 @@ class RetryDialog extends Component {
18
18
  event: PropTypes.shape({
19
19
  id: PropTypes.number,
20
20
  retryVisible: PropTypes.bool
21
- })
21
+ }).isRequired
22
22
  }
23
23
  }
24
24
 
@@ -0,0 +1,19 @@
1
+ import React, { Component, PropTypes } from 'react'
2
+ import ErrorSVG from 'material-ui/svg-icons/alert/error'
3
+
4
+ export default class extends Component {
5
+ static get propTypes () {
6
+ return {
7
+ message: PropTypes.string
8
+ }
9
+ }
10
+
11
+ render () {
12
+ return this.props.message ? (
13
+ <div className='event-error-message'>
14
+ <ErrorSVG style={{marginRight: '10px'}}/>
15
+ <span>{this.props.message}</span>
16
+ </div>
17
+ ) : null
18
+ }
19
+ }
@@ -0,0 +1,8 @@
1
+ .event-error-message {
2
+ display: flex;
3
+ text-align: left;
4
+
5
+ & > span {
6
+ color: #f44336;
7
+ }
8
+ }
@@ -0,0 +1,3 @@
1
+ .event {
2
+ cursor: pointer;
3
+ }
@@ -0,0 +1,82 @@
1
+ import React, { Component, PropTypes } from 'react'
2
+ import { connect } from 'react-redux'
3
+ import moment from 'moment'
4
+
5
+ import { showFailureOverview } from 'actions/failures/overview'
6
+ import style from 'components/event/style'
7
+
8
+ import { Card, CardHeader, CardTitle } from 'material-ui/Card'
9
+ import FailureOverviewDialog from 'components/failure/overview-dialog'
10
+ import FailureRetryDialog from 'components/failure/retry-dialog'
11
+
12
+ const TIME_FORMAT = 'h:mm:ss a'
13
+ const EMPTY_TYPE = '<no type>'
14
+
15
+ export function formatTime (time) {
16
+ if (!time) return null
17
+ const timeDate = new Date(time)
18
+ return moment(timeDate).format(TIME_FORMAT)
19
+ }
20
+
21
+ export class Failure extends Component {
22
+ static get propTypes () {
23
+ return {
24
+ onShowOverview: PropTypes.func,
25
+ failure: PropTypes.shape({
26
+ id: PropTypes.number,
27
+ created_at: PropTypes.string,
28
+ topic: PropTypes.string,
29
+ group_id: PropTypes.string,
30
+ entity_id: PropTypes.string,
31
+ event_time: PropTypes.string,
32
+ event_type: PropTypes.string,
33
+ event_version: PropTypes.string,
34
+ checksum: PropTypes.string,
35
+ payload: PropTypes.object,
36
+ metadata: PropTypes.object,
37
+ error_class: PropTypes.string,
38
+ error_message: PropTypes.string,
39
+ error_backtrace: PropTypes.array
40
+ })
41
+ }
42
+ }
43
+
44
+ static get defaultProps () {
45
+ return {
46
+ failure: {}
47
+ }
48
+ }
49
+
50
+ render () {
51
+ return (
52
+ <Card
53
+ className='failure'
54
+ style={style.card}
55
+ onClick={() => this.showOverview()}>
56
+ <CardHeader
57
+ className='failure-header'
58
+ titleStyle={style.cardHeader.title}
59
+ subtitleStyle={style.cardHeader.subtitle}
60
+ title={formatTime(this.props.failure.event_time)}
61
+ subtitle={this.props.failure.topic}/>
62
+ <CardTitle
63
+ titleStyle={style.cardTitle}
64
+ title={this.formattedEventType()}/>
65
+ <FailureOverviewDialog failure={this.props.failure} />
66
+ <FailureRetryDialog failure={this.props.failure} />
67
+ </Card>
68
+ )
69
+ }
70
+
71
+ showOverview () {
72
+ this.props.onShowOverview(this.props.failure)
73
+ }
74
+
75
+ formattedEventType () {
76
+ return this.props.failure.event_type || EMPTY_TYPE
77
+ }
78
+ }
79
+
80
+ export default connect((state, ownProps) => ownProps, {
81
+ onShowOverview: showFailureOverview
82
+ })(Failure)
@@ -0,0 +1,89 @@
1
+ import React from 'react'
2
+ import jasmineEnzyme from 'jasmine-enzyme'
3
+ import { mount } from 'enzyme'
4
+ import { Event, formatEventTime } from 'components/event'
5
+ import getMuiTheme from 'material-ui/styles/getMuiTheme'
6
+ import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'
7
+ import { Provider } from 'react-redux'
8
+ import configureMockStore from 'redux-mock-store'
9
+ import thunk from 'redux-thunk'
10
+
11
+ const middlewares = [ thunk ]
12
+ const mockStore = configureMockStore(middlewares)
13
+
14
+ const mountComponent = (store, props) => mount(
15
+ <Provider store={store}>
16
+ <MuiThemeProvider muiTheme={getMuiTheme()}>
17
+ <Event {...props} />
18
+ </MuiThemeProvider>
19
+ </Provider>
20
+ )
21
+
22
+ describe('<Event />', () => {
23
+ let props, component, store, onShowOverview
24
+
25
+ beforeEach(() => {
26
+ jasmineEnzyme()
27
+ store = mockStore({
28
+ xhrStatus: {
29
+ isRetryingEvent: false
30
+ }
31
+ })
32
+
33
+ onShowOverview = jasmine.createSpy('onShowOverview')
34
+
35
+ props = {
36
+ onShowOverview: onShowOverview,
37
+ event: {
38
+ id: 1,
39
+ topic: 'phobos.test',
40
+ group_id: 'phobos-checkpoint-consumer',
41
+ entity_id: 'a5dbd02d-bc40-6d15-b993-83a4825d94e6',
42
+ event_time: '2016-09-23T21:00:40.515Z',
43
+ event_type: 'order-placed',
44
+ event_version: 'v1',
45
+ checksum: '188773471ec0f898fd81d272760a027f',
46
+ payload: { data: { name: 'phobos' } }
47
+ }
48
+ }
49
+
50
+ component = mountComponent(store, props)
51
+ })
52
+
53
+ it('displays event_time formatted', () => {
54
+ expect(component.text()).toMatch(formatEventTime(props.event.event_time))
55
+ })
56
+
57
+ it('displays topic', () => {
58
+ expect(component.text()).toMatch('phobos.test')
59
+ })
60
+
61
+ it('displays event_type', () => {
62
+ expect(component.text()).toMatch('order-placed')
63
+ })
64
+
65
+ describe('when clicked', () => {
66
+ it('calls onShowOverview with the event', () => {
67
+ component.find('.event').simulate('click')
68
+ expect(onShowOverview).toHaveBeenCalledWith(props.event)
69
+ })
70
+ })
71
+
72
+ describe('when event has overviewVisible=true', () => {
73
+ let dialog
74
+ beforeEach(() => {
75
+ Object.assign(props.event, { overviewVisible: true })
76
+ component = mountComponent(store, props)
77
+ const dialogs = document.getElementsByClassName('event-overview-dialog')
78
+ dialog = dialogs[dialogs.length - 1]
79
+ })
80
+
81
+ it('opens the event overview dialog', () => {
82
+ expect(dialog).not.toBe(null)
83
+ })
84
+
85
+ it('displays event_id', () => {
86
+ expect(dialog.innerText).toMatch(`#${props.event.id}`)
87
+ })
88
+ })
89
+ })
@@ -0,0 +1,16 @@
1
+ import React, { Component, PropTypes } from 'react'
2
+ import CircularProgress from 'material-ui/CircularProgress'
3
+
4
+ export default class extends Component {
5
+ static get propTypes () {
6
+ return {
7
+ visible: PropTypes.bool.isRequired
8
+ }
9
+ }
10
+
11
+ render () {
12
+ return this.props.visible
13
+ ? <CircularProgress />
14
+ : null
15
+ }
16
+ }
@@ -0,0 +1,28 @@
1
+ .failure-overview {
2
+ .detail {
3
+ margin-bottom: 20px;
4
+
5
+ &:first-child {
6
+ margin-top: 20px;
7
+ }
8
+
9
+ p {
10
+ margin: 0;
11
+ }
12
+
13
+ label {
14
+ font-weight: lighter;
15
+ text-transform: uppercase;
16
+ }
17
+
18
+ .value {
19
+ font-weight: bold;
20
+ }
21
+ }
22
+
23
+ .json-pretty {
24
+ margin-top: 0;
25
+ padding: 10px;
26
+ white-space: pre-wrap;
27
+ }
28
+ }