phobos_checkpoint_ui 0.4.0 → 1.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
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
+ }