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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +5 -1
- data/frontend/.gitignore +4 -0
- data/frontend/src/actions/events-search.js +11 -11
- data/frontend/src/actions/events-search.spec.js +21 -21
- data/frontend/src/actions/failures/details.js +42 -0
- data/frontend/src/actions/failures/details.spec.js +71 -0
- data/frontend/src/actions/failures/overview.js +14 -0
- data/frontend/src/actions/failures/overview.spec.js +20 -0
- data/frontend/src/actions/failures/retry.js +58 -0
- data/frontend/src/actions/failures/retry.spec.js +117 -0
- data/frontend/src/actions/failures/search/index.js +76 -0
- data/frontend/src/actions/failures/search/index.spec.js +197 -0
- data/frontend/src/actions/index.js +24 -5
- data/frontend/src/actions/navigation/index.js +5 -0
- data/frontend/src/actions/navigation/index.spec.js +21 -0
- data/frontend/src/api.js +11 -0
- data/frontend/src/components/{event-overview/attribute.js → attribute/index.js} +0 -0
- data/frontend/src/components/empty-event/index.js +20 -0
- data/frontend/src/components/empty-event/index.spec.js +38 -0
- data/frontend/src/components/event-overview/index.js +1 -1
- data/frontend/src/components/event-retry-dialog/index.js +1 -1
- data/frontend/src/components/failure/error-message.js +19 -0
- data/frontend/src/components/failure/error-message.scss +8 -0
- data/frontend/src/components/failure/event.scss +3 -0
- data/frontend/src/components/failure/index.js +82 -0
- data/frontend/src/components/failure/index.spec.js +89 -0
- data/frontend/src/components/failure/loading.js +16 -0
- data/frontend/src/components/failure/overview/failure-overview.scss +28 -0
- data/frontend/src/components/failure/overview/index.js +60 -0
- data/frontend/src/components/failure/overview/index.spec.js +71 -0
- data/frontend/src/components/failure/overview-dialog/event-overview-dialog.scss +15 -0
- data/frontend/src/components/failure/overview-dialog/index.js +90 -0
- data/frontend/src/components/failure/retry-dialog/index.js +72 -0
- data/frontend/src/components/failure/style.js +16 -0
- data/frontend/src/components/failures-list/failures-list.scss +49 -0
- data/frontend/src/components/failures-list/index.js +62 -0
- data/frontend/src/components/failures-list/index.spec.js +59 -0
- data/frontend/src/components/header/index.js +36 -2
- data/frontend/src/components/load-more/index.js +22 -0
- data/frontend/src/components/load-more/index.spec.js +54 -0
- data/frontend/src/components/search-input/index.js +2 -5
- data/frontend/src/components/search-input/index.spec.js +6 -6
- data/frontend/src/reducers/events.js +2 -2
- data/frontend/src/reducers/events.spec.js +4 -4
- data/frontend/src/reducers/failures/details/index.js +32 -0
- data/frontend/src/reducers/failures/details/index.spec.js +54 -0
- data/frontend/src/reducers/failures/index.js +48 -0
- data/frontend/src/reducers/failures/index.spec.js +95 -0
- data/frontend/src/reducers/index.js +4 -1
- data/frontend/src/reducers/index.spec.js +2 -0
- data/frontend/src/reducers/xhr-status.js +25 -10
- data/frontend/src/reducers/xhr-status.spec.js +65 -15
- data/frontend/src/routes.js +6 -2
- data/frontend/src/store.js +7 -3
- data/frontend/src/views/{event-details.js → events/details/index.js} +0 -0
- data/frontend/src/views/{events-search.js → events/search/index.js} +37 -44
- data/frontend/src/views/{events-search.scss → events/search/index.scss} +0 -0
- data/frontend/src/views/{events-search.spec.js → events/search/index.spec.js} +35 -25
- data/frontend/src/views/failures/details/index.js +50 -0
- data/frontend/src/views/failures/search/index.js +113 -0
- data/frontend/src/views/failures/search/index.scss +24 -0
- data/frontend/src/views/failures/search/index.spec.js +106 -0
- data/lib/phobos_checkpoint_ui/version.rb +1 -1
- data/phobos_checkpoint_ui.gemspec +1 -1
- 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
|
14
|
-
export const
|
15
|
-
export const
|
16
|
-
export const
|
17
|
-
export const
|
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,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
|
})
|
File without changes
|
@@ -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/
|
6
|
+
import Attribute from 'components/attribute'
|
7
7
|
|
8
8
|
const EVENT_TIME_FORMAT = 'MMMM Do YYYY, h:mm:ss a'
|
9
9
|
|
@@ -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,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
|
+
}
|