phobos_checkpoint_ui 0.1.0
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 +7 -0
- data/.gitignore +15 -0
- data/.rspec +2 -0
- data/CHANGELOG.md +9 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +81 -0
- data/Rakefile +12 -0
- data/assets/index-52cbf3063583f3c09a4b-0.css +2 -0
- data/assets/index-52cbf3063583f3c09a4b-0.css.map +1 -0
- data/assets/index-52cbf3063583f3c09a4b-1.css +2 -0
- data/assets/index-52cbf3063583f3c09a4b-1.css.map +1 -0
- data/assets/index-52cbf3063583f3c09a4b.js +49 -0
- data/assets/index-52cbf3063583f3c09a4b.js.map +1 -0
- data/assets/index.html +12 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/circle.yml +29 -0
- data/frontend/.babelrc +3 -0
- data/frontend/.editorconfig +8 -0
- data/frontend/.eslintignore +2 -0
- data/frontend/.eslintrc +3 -0
- data/frontend/.npmignore +8 -0
- data/frontend/package.json +45 -0
- data/frontend/sagui.config.js +52 -0
- data/frontend/src/actions/event-details.js +42 -0
- data/frontend/src/actions/event-details.spec.js +71 -0
- data/frontend/src/actions/event-overview.js +11 -0
- data/frontend/src/actions/event-overview.spec.js +20 -0
- data/frontend/src/actions/event-retry.js +58 -0
- data/frontend/src/actions/event-retry.spec.js +117 -0
- data/frontend/src/actions/events-search.js +75 -0
- data/frontend/src/actions/events-search.spec.js +197 -0
- data/frontend/src/actions/flash-messages.js +16 -0
- data/frontend/src/actions/flash-messages.spec.js +23 -0
- data/frontend/src/actions/index.js +24 -0
- data/frontend/src/actions/search-input-filter.js +14 -0
- data/frontend/src/actions/search-input-filter.spec.js +20 -0
- data/frontend/src/api.js +46 -0
- data/frontend/src/components/event/error-message.js +19 -0
- data/frontend/src/components/event/error-message.scss +8 -0
- data/frontend/src/components/event/event.scss +3 -0
- data/frontend/src/components/event/index.js +77 -0
- data/frontend/src/components/event/index.spec.js +89 -0
- data/frontend/src/components/event/loading.js +16 -0
- data/frontend/src/components/event/style.js +16 -0
- data/frontend/src/components/event-overview/attribute.js +24 -0
- data/frontend/src/components/event-overview/event-overview.scss +28 -0
- data/frontend/src/components/event-overview/index.js +47 -0
- data/frontend/src/components/event-overview/index.spec.js +67 -0
- data/frontend/src/components/event-overview-dialog/event-overview-dialog.scss +15 -0
- data/frontend/src/components/event-overview-dialog/index.js +85 -0
- data/frontend/src/components/event-retry-dialog/index.js +72 -0
- data/frontend/src/components/events-list/events-list.scss +49 -0
- data/frontend/src/components/events-list/index.js +62 -0
- data/frontend/src/components/events-list/index.spec.js +59 -0
- data/frontend/src/components/flash-message/flash-message.scss +40 -0
- data/frontend/src/components/flash-message/index.js +45 -0
- data/frontend/src/components/flash-message/index.spec.js +59 -0
- data/frontend/src/components/flash-message-list/index.js +34 -0
- data/frontend/src/components/header/header.scss +29 -0
- data/frontend/src/components/header/index.js +44 -0
- data/frontend/src/components/search-input/index.js +104 -0
- data/frontend/src/components/search-input/index.spec.js +56 -0
- data/frontend/src/components/search-input/search-input.scss +7 -0
- data/frontend/src/configs.js +15 -0
- data/frontend/src/helpers.spec.js +2 -0
- data/frontend/src/index.html +12 -0
- data/frontend/src/index.js +26 -0
- data/frontend/src/index.scss +31 -0
- data/frontend/src/reducers/event-details.js +32 -0
- data/frontend/src/reducers/event-details.spec.js +54 -0
- data/frontend/src/reducers/events-filters.js +17 -0
- data/frontend/src/reducers/events-filters.spec.js +34 -0
- data/frontend/src/reducers/events.js +48 -0
- data/frontend/src/reducers/events.spec.js +95 -0
- data/frontend/src/reducers/flash-messages.js +14 -0
- data/frontend/src/reducers/flash-messages.spec.js +34 -0
- data/frontend/src/reducers/index.js +18 -0
- data/frontend/src/reducers/index.spec.js +20 -0
- data/frontend/src/reducers/xhr-status.js +75 -0
- data/frontend/src/reducers/xhr-status.spec.js +94 -0
- data/frontend/src/routes.js +20 -0
- data/frontend/src/store.js +15 -0
- data/frontend/src/views/event-details.js +50 -0
- data/frontend/src/views/events-search.js +112 -0
- data/frontend/src/views/events-search.scss +24 -0
- data/frontend/src/views/events-search.spec.js +96 -0
- data/frontend/src/views/layout.js +24 -0
- data/frontend/src/views/layout.scss +3 -0
- data/lib/phobos_checkpoint_ui/app.rb +11 -0
- data/lib/phobos_checkpoint_ui/static_app.rb +19 -0
- data/lib/phobos_checkpoint_ui/tasks.rb +20 -0
- data/lib/phobos_checkpoint_ui/version.rb +3 -0
- data/lib/phobos_checkpoint_ui.rb +10 -0
- data/phobos_checkpoint_ui.gemspec +53 -0
- data/screenshot1.png +0 -0
- data/screenshot2.png +0 -0
- metadata +267 -0
|
@@ -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_SEARCH,
|
|
13
|
+
REQUEST_SEARCH_RESULTS,
|
|
14
|
+
RECEIVE_SEARCH_RESULTS,
|
|
15
|
+
REQUEST_SEARCH_RESULTS_FAILED,
|
|
16
|
+
ADD_FLASH_MESSAGE,
|
|
17
|
+
LOAD_MORE_SEARCH_RESULTS
|
|
18
|
+
} from 'actions'
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
triggerSearch,
|
|
22
|
+
fetchSearchResults,
|
|
23
|
+
loadMoreSearchResults
|
|
24
|
+
} from 'actions/events-search'
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
Mappersmith.Env.Fixture.clear()
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
describe('actions/event-search', () => {
|
|
31
|
+
describe('#fetchSearchResults', () => {
|
|
32
|
+
describe('without filters', () => {
|
|
33
|
+
let event, initialState, store
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
initialState = { eventsFilters: {}, xhrStatus: { currentEventsOffset: 0 } }
|
|
36
|
+
store = mockStore(initialState)
|
|
37
|
+
event = { id: 1 }
|
|
38
|
+
Mappersmith.Env.Fixture
|
|
39
|
+
.define('get')
|
|
40
|
+
.matching({ url: `/api/v1/events?limit=${EVENTS_SEARCH_LIMIT}&offset=0` })
|
|
41
|
+
.response([event])
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('creates REQUEST and RECEIVE actions', (done) => {
|
|
45
|
+
store.dispatch(fetchSearchResults(event)).then(() => {
|
|
46
|
+
const actions = store.getActions()
|
|
47
|
+
expect(actions[0]).toEqual({ type: REQUEST_SEARCH_RESULTS })
|
|
48
|
+
expect(actions[1]).toEqual({ type: RECEIVE_SEARCH_RESULTS, events: [event], 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 event, initialState, store
|
|
57
|
+
beforeEach(() => {
|
|
58
|
+
initialState = {
|
|
59
|
+
eventsFilters: { type: 'event_type', value: 'new' },
|
|
60
|
+
xhrStatus: { currentEventsOffset: 0 }
|
|
61
|
+
}
|
|
62
|
+
store = mockStore(initialState)
|
|
63
|
+
event = { id: 1 }
|
|
64
|
+
Mappersmith.Env.Fixture
|
|
65
|
+
.define('get')
|
|
66
|
+
.matching({ url: `/api/v1/events?limit=${EVENTS_SEARCH_LIMIT}&event_type=new&offset=0` })
|
|
67
|
+
.response([event])
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('creates REQUEST and RECEIVE actions using the filters', (done) => {
|
|
71
|
+
store.dispatch(fetchSearchResults(event)).then(() => {
|
|
72
|
+
const actions = store.getActions()
|
|
73
|
+
expect(actions[0]).toEqual({ type: REQUEST_SEARCH_RESULTS })
|
|
74
|
+
expect(actions[1]).toEqual({ type: RECEIVE_SEARCH_RESULTS, events: [event], 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 event, initialState, store
|
|
83
|
+
beforeEach(() => {
|
|
84
|
+
initialState = {
|
|
85
|
+
eventsFilters: { },
|
|
86
|
+
xhrStatus: { currentEventsOffset: 4 }
|
|
87
|
+
}
|
|
88
|
+
store = mockStore(initialState)
|
|
89
|
+
event = { id: 1 }
|
|
90
|
+
Mappersmith.Env.Fixture
|
|
91
|
+
.define('get')
|
|
92
|
+
.matching({ url: `/api/v1/events?limit=${EVENTS_SEARCH_LIMIT}&offset=4` })
|
|
93
|
+
.response([event])
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('creates REQUEST and RECEIVE actions pointing to the correct offset', (done) => {
|
|
97
|
+
store.dispatch(fetchSearchResults(event)).then(() => {
|
|
98
|
+
const actions = store.getActions()
|
|
99
|
+
expect(actions[0]).toEqual({ type: REQUEST_SEARCH_RESULTS })
|
|
100
|
+
expect(actions[1]).toEqual({ type: RECEIVE_SEARCH_RESULTS, events: [event], 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 event, initialState, store
|
|
109
|
+
beforeEach(() => {
|
|
110
|
+
initialState = { eventsFilters: {}, xhrStatus: { currentEventsOffset: 0 } }
|
|
111
|
+
store = mockStore(initialState)
|
|
112
|
+
event = { id: 1 }
|
|
113
|
+
Mappersmith.Env.Fixture
|
|
114
|
+
.define('get')
|
|
115
|
+
.matching({ url: `/api/v1/events?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(event)).then(() => {
|
|
127
|
+
const actions = store.getActions()
|
|
128
|
+
expect(actions[0]).toEqual({ type: REQUEST_SEARCH_RESULTS })
|
|
129
|
+
expect(actions[1]).toEqual({
|
|
130
|
+
type: REQUEST_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: 'Events 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 event, initialState, store
|
|
146
|
+
beforeEach(() => {
|
|
147
|
+
initialState = {
|
|
148
|
+
eventsFilters: {},
|
|
149
|
+
xhrStatus: { currentEventsOffset: 0 }
|
|
150
|
+
}
|
|
151
|
+
store = mockStore(initialState)
|
|
152
|
+
event = { id: 1 }
|
|
153
|
+
Mappersmith.Env.Fixture
|
|
154
|
+
.define('get')
|
|
155
|
+
.matching({ url: `/api/v1/events?limit=${EVENTS_SEARCH_LIMIT}&offset=0` })
|
|
156
|
+
.response([event])
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('creates TRIGGER_SEARCH and REQUEST actions', (done) => {
|
|
160
|
+
store.dispatch(triggerSearch()).then(() => {
|
|
161
|
+
const actions = store.getActions()
|
|
162
|
+
expect(actions[0]).toEqual({ type: TRIGGER_SEARCH })
|
|
163
|
+
expect(actions[1]).toEqual({ type: REQUEST_SEARCH_RESULTS })
|
|
164
|
+
expect(actions[2]).toEqual({ type: RECEIVE_SEARCH_RESULTS, events: [event], 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 event, initialState, store
|
|
173
|
+
beforeEach(() => {
|
|
174
|
+
initialState = {
|
|
175
|
+
eventsFilters: {},
|
|
176
|
+
xhrStatus: { currentEventsOffset: 4 }
|
|
177
|
+
}
|
|
178
|
+
store = mockStore(initialState)
|
|
179
|
+
event = { id: 1 }
|
|
180
|
+
Mappersmith.Env.Fixture
|
|
181
|
+
.define('get')
|
|
182
|
+
.matching({ url: `/api/v1/events?limit=${EVENTS_SEARCH_LIMIT}&offset=4` })
|
|
183
|
+
.response([event])
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('creates LOAD_MORE_SEARCH_RESULTS and REQUEST actions', (done) => {
|
|
187
|
+
store.dispatch(loadMoreSearchResults()).then(() => {
|
|
188
|
+
const actions = store.getActions()
|
|
189
|
+
expect(actions[0]).toEqual({ type: LOAD_MORE_SEARCH_RESULTS, offset: 4 + EVENTS_SEARCH_LIMIT })
|
|
190
|
+
expect(actions[1]).toEqual({ type: REQUEST_SEARCH_RESULTS })
|
|
191
|
+
expect(actions[2]).toEqual({ type: RECEIVE_SEARCH_RESULTS, events: [event], offset: 4 })
|
|
192
|
+
done()
|
|
193
|
+
})
|
|
194
|
+
.catch((e) => done.fail(`test failed with promise error: ${e.message}`))
|
|
195
|
+
})
|
|
196
|
+
})
|
|
197
|
+
})
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
let ids = 0
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
ADD_FLASH_MESSAGE,
|
|
5
|
+
DELETE_FLASH_MESSAGE
|
|
6
|
+
} from 'actions'
|
|
7
|
+
|
|
8
|
+
export const addFlashMessage = (message) => ({
|
|
9
|
+
type: ADD_FLASH_MESSAGE,
|
|
10
|
+
message: Object.assign({ id: `${++ids}` }, message)
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
export const deleteFlashMessage = (id) => ({
|
|
14
|
+
type: DELETE_FLASH_MESSAGE,
|
|
15
|
+
id: id
|
|
16
|
+
})
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ADD_FLASH_MESSAGE, DELETE_FLASH_MESSAGE } from 'actions'
|
|
2
|
+
import { addFlashMessage, deleteFlashMessage } from 'actions/flash-messages'
|
|
3
|
+
|
|
4
|
+
describe('actions/flash-messages', () => {
|
|
5
|
+
describe('#addFlashMessage', () => {
|
|
6
|
+
it('creates an action to add flash messags auto generating the id', () => {
|
|
7
|
+
const message = { type: 'success', text: 'congrats!' }
|
|
8
|
+
const expectedAction = {
|
|
9
|
+
type: ADD_FLASH_MESSAGE,
|
|
10
|
+
message: Object.assign({ id: jasmine.any(String) }, message)
|
|
11
|
+
}
|
|
12
|
+
expect(addFlashMessage(message)).toEqual(expectedAction)
|
|
13
|
+
})
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
describe('#addFlashMessage', () => {
|
|
17
|
+
it('creates an action to delete flash messags', () => {
|
|
18
|
+
const id = '1'
|
|
19
|
+
const expectedAction = { type: DELETE_FLASH_MESSAGE, id: id }
|
|
20
|
+
expect(deleteFlashMessage(id)).toEqual(expectedAction)
|
|
21
|
+
})
|
|
22
|
+
})
|
|
23
|
+
})
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export const EVENT_SHOW_OVERVIEW = 'EVENT_SHOW_OVERVIEW'
|
|
2
|
+
export const EVENT_HIDE_OVERVIEW = 'EVENT_HIDE_OVERVIEW'
|
|
3
|
+
|
|
4
|
+
export const EVENT_SHOW_RETRY = 'EVENT_SHOW_RETRY'
|
|
5
|
+
export const EVENT_HIDE_RETRY = 'EVENT_HIDE_RETRY'
|
|
6
|
+
export const REQUEST_EVENT_RETRY = 'REQUEST_EVENT_RETRY'
|
|
7
|
+
export const RECEIVE_EVENT_RETRY = 'RECEIVE_EVENT_RETRY'
|
|
8
|
+
export const REQUEST_EVENT_RETRY_FAILED = 'REQUEST_EVENT_RETRY_FAILED'
|
|
9
|
+
|
|
10
|
+
export const SEARCH_INPUT_CHANGE_FILTER_TYPE = 'SEARCH_INPUT_CHANGE_FILTER_TYPE'
|
|
11
|
+
export const SEARCH_INPUT_CHANGE_FILTER_VALUE = 'SEARCH_INPUT_CHANGE_FILTER_VALUE'
|
|
12
|
+
|
|
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'
|
|
18
|
+
|
|
19
|
+
export const ADD_FLASH_MESSAGE = 'ADD_FLASH_MESSAGE'
|
|
20
|
+
export const DELETE_FLASH_MESSAGE = 'DELETE_FLASH_MESSAGE'
|
|
21
|
+
|
|
22
|
+
export const REQUEST_EVENT_DETAILS = 'REQUEST_EVENT_DETAILS'
|
|
23
|
+
export const RECEIVE_EVENT_DETAILS = 'RECEIVE_EVENT_DETAILS'
|
|
24
|
+
export const REQUEST_EVENT_DETAILS_FAILED = 'REQUEST_EVENT_DETAILS_FAILED'
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SEARCH_INPUT_CHANGE_FILTER_TYPE,
|
|
3
|
+
SEARCH_INPUT_CHANGE_FILTER_VALUE
|
|
4
|
+
} from 'actions'
|
|
5
|
+
|
|
6
|
+
export const changeSearchInputFilterType = (filterType) => ({
|
|
7
|
+
type: SEARCH_INPUT_CHANGE_FILTER_TYPE,
|
|
8
|
+
filterType
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
export const changeSearchInputFilterValue = (filterValue) => ({
|
|
12
|
+
type: SEARCH_INPUT_CHANGE_FILTER_VALUE,
|
|
13
|
+
filterValue
|
|
14
|
+
})
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { SEARCH_INPUT_CHANGE_FILTER_TYPE, SEARCH_INPUT_CHANGE_FILTER_VALUE } from 'actions'
|
|
2
|
+
import { changeSearchInputFilterType, changeSearchInputFilterValue } from 'actions/search-input-filter'
|
|
3
|
+
|
|
4
|
+
describe('actions/search-input-filter', () => {
|
|
5
|
+
describe('#changeSearchInputFilterType', () => {
|
|
6
|
+
it('creates an action to change the filter type', () => {
|
|
7
|
+
const type = 'entity_id'
|
|
8
|
+
const expectedAction = { type: SEARCH_INPUT_CHANGE_FILTER_TYPE, filterType: type }
|
|
9
|
+
expect(changeSearchInputFilterType(type)).toEqual(expectedAction)
|
|
10
|
+
})
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
describe('#changeSearchInputFilterValue', () => {
|
|
14
|
+
it('creates an action to change the filter value', () => {
|
|
15
|
+
const value = '123abc'
|
|
16
|
+
const expectedAction = { type: SEARCH_INPUT_CHANGE_FILTER_VALUE, filterValue: value }
|
|
17
|
+
expect(changeSearchInputFilterValue(value)).toEqual(expectedAction)
|
|
18
|
+
})
|
|
19
|
+
})
|
|
20
|
+
})
|
data/frontend/src/api.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import Mappersmith from 'mappersmith'
|
|
2
|
+
|
|
3
|
+
Mappersmith.Env.USE_PROMISES = true
|
|
4
|
+
|
|
5
|
+
export const EVENTS_SEARCH_LIMIT = 20
|
|
6
|
+
|
|
7
|
+
export function parseResponseError (response) {
|
|
8
|
+
let error
|
|
9
|
+
|
|
10
|
+
if (response.err) {
|
|
11
|
+
const errorObj = response.err[0]
|
|
12
|
+
if (errorObj.responseText) {
|
|
13
|
+
try {
|
|
14
|
+
error = JSON.parse(errorObj.responseText)
|
|
15
|
+
} catch (e) {
|
|
16
|
+
error = { message: errorObj.responseText }
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
} else if (response.message) {
|
|
20
|
+
error = response
|
|
21
|
+
} else {
|
|
22
|
+
error = { message: response }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return error
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export default Mappersmith.forge({
|
|
29
|
+
host: false,
|
|
30
|
+
resources: {
|
|
31
|
+
Config: {
|
|
32
|
+
load: '/configs'
|
|
33
|
+
},
|
|
34
|
+
Event: {
|
|
35
|
+
findById: '/api/v1/events/{id}',
|
|
36
|
+
retry: {
|
|
37
|
+
path: '/api/v1/events/{id}/retry',
|
|
38
|
+
method: 'POST'
|
|
39
|
+
},
|
|
40
|
+
search: {
|
|
41
|
+
path: '/api/v1/events',
|
|
42
|
+
params: { limit: EVENTS_SEARCH_LIMIT }
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
})
|
|
@@ -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,77 @@
|
|
|
1
|
+
import React, { Component, PropTypes } from 'react'
|
|
2
|
+
import { connect } from 'react-redux'
|
|
3
|
+
import moment from 'moment'
|
|
4
|
+
|
|
5
|
+
import { showEventOverview } from 'actions/event-overview'
|
|
6
|
+
import style from 'components/event/style'
|
|
7
|
+
|
|
8
|
+
import {Card, CardHeader, CardTitle} from 'material-ui/Card'
|
|
9
|
+
import EventOverviewDialog from 'components/event-overview-dialog'
|
|
10
|
+
import EventRetryDialog from 'components/event-retry-dialog'
|
|
11
|
+
|
|
12
|
+
const EVENT_TIME_FORMAT = 'h:mm:ss a'
|
|
13
|
+
const EMPTY_EVENT_TYPE = '<no type>'
|
|
14
|
+
|
|
15
|
+
export function formatEventTime (eventTime) {
|
|
16
|
+
if (!eventTime) return null
|
|
17
|
+
const eventTimeDate = new Date(eventTime)
|
|
18
|
+
return moment(eventTimeDate).format(EVENT_TIME_FORMAT)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class Event extends Component {
|
|
22
|
+
static get propTypes () {
|
|
23
|
+
return {
|
|
24
|
+
onShowOverview: PropTypes.func,
|
|
25
|
+
event: PropTypes.shape({
|
|
26
|
+
id: PropTypes.number,
|
|
27
|
+
group_id: PropTypes.string,
|
|
28
|
+
topic: PropTypes.string,
|
|
29
|
+
entity_id: PropTypes.string,
|
|
30
|
+
event_type: PropTypes.string,
|
|
31
|
+
event_time: PropTypes.string,
|
|
32
|
+
event_version: PropTypes.string,
|
|
33
|
+
checksum: PropTypes.string,
|
|
34
|
+
payload: PropTypes.object
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
static get defaultProps () {
|
|
40
|
+
return {
|
|
41
|
+
event: {}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
render () {
|
|
46
|
+
return (
|
|
47
|
+
<Card
|
|
48
|
+
className='event'
|
|
49
|
+
style={style.card}
|
|
50
|
+
onClick={() => this.showOverview()}>
|
|
51
|
+
<CardHeader
|
|
52
|
+
className='event-header'
|
|
53
|
+
titleStyle={style.cardHeader.title}
|
|
54
|
+
subtitleStyle={style.cardHeader.subtitle}
|
|
55
|
+
title={formatEventTime(this.props.event.event_time)}
|
|
56
|
+
subtitle={this.props.event.topic}/>
|
|
57
|
+
<CardTitle
|
|
58
|
+
titleStyle={style.cardTitle}
|
|
59
|
+
title={this.formatedEventType()}/>
|
|
60
|
+
<EventOverviewDialog event={this.props.event} />
|
|
61
|
+
<EventRetryDialog event={this.props.event} />
|
|
62
|
+
</Card>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
showOverview () {
|
|
67
|
+
this.props.onShowOverview(this.props.event)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
formatedEventType () {
|
|
71
|
+
return this.props.event.event_type || EMPTY_EVENT_TYPE
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export default connect((state, ownProps) => ownProps, {
|
|
76
|
+
onShowOverview: showEventOverview
|
|
77
|
+
})(Event)
|
|
@@ -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,16 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
card: {
|
|
3
|
+
width: '490px',
|
|
4
|
+
overflow: 'hidden',
|
|
5
|
+
backgroundColor: '#fff'
|
|
6
|
+
},
|
|
7
|
+
cardHeader: {
|
|
8
|
+
title: {color: '#000', fontWeight: 'lighter'},
|
|
9
|
+
subtitle: {color: '#000'}
|
|
10
|
+
},
|
|
11
|
+
cardTitle: {
|
|
12
|
+
color: '#000',
|
|
13
|
+
fontSize: '38px',
|
|
14
|
+
fontWeight: 'lighter'
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import React, { Component, PropTypes } from 'react'
|
|
2
|
+
|
|
3
|
+
export default class extends Component {
|
|
4
|
+
static get propTypes () {
|
|
5
|
+
return {
|
|
6
|
+
label: PropTypes.string,
|
|
7
|
+
value: PropTypes.string
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
render () {
|
|
12
|
+
const { label, value, children } = this.props
|
|
13
|
+
const body = children || null
|
|
14
|
+
return (value || body) &&
|
|
15
|
+
<div className='detail'>
|
|
16
|
+
<p><label>{label}</label></p>
|
|
17
|
+
{
|
|
18
|
+
value
|
|
19
|
+
? <p className='value'>{value}</p>
|
|
20
|
+
: body
|
|
21
|
+
}
|
|
22
|
+
</div>
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
.event-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
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import React, { Component, PropTypes } from 'react'
|
|
2
|
+
import moment from 'moment'
|
|
3
|
+
import JSONPretty from 'react-json-pretty'
|
|
4
|
+
import 'react-json-pretty/src/JSONPretty.monikai.css'
|
|
5
|
+
|
|
6
|
+
import Attribute from 'components/event-overview/attribute'
|
|
7
|
+
|
|
8
|
+
const EVENT_TIME_FORMAT = 'MMMM Do YYYY, h:mm:ss a'
|
|
9
|
+
|
|
10
|
+
export function formatEventTime (eventTime) {
|
|
11
|
+
if (!eventTime) return null
|
|
12
|
+
const eventTimeDate = new Date(eventTime)
|
|
13
|
+
return moment(eventTimeDate).format(EVENT_TIME_FORMAT)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default class extends Component {
|
|
17
|
+
static get propTypes () {
|
|
18
|
+
return {
|
|
19
|
+
id: PropTypes.number,
|
|
20
|
+
group_id: PropTypes.string,
|
|
21
|
+
topic: PropTypes.string,
|
|
22
|
+
entity_id: PropTypes.string,
|
|
23
|
+
event_type: PropTypes.string,
|
|
24
|
+
event_time: PropTypes.string,
|
|
25
|
+
event_version: PropTypes.string,
|
|
26
|
+
checksum: PropTypes.string,
|
|
27
|
+
payload: PropTypes.object
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
render () {
|
|
32
|
+
return (
|
|
33
|
+
<div className='event-overview'>
|
|
34
|
+
<Attribute label='Group ID' value={this.props.group_id} />
|
|
35
|
+
<Attribute label='Topic' value={this.props.topic} />
|
|
36
|
+
<Attribute label='Entity ID' value={this.props.entity_id} />
|
|
37
|
+
<Attribute label='Event Type' value={this.props.event_type} />
|
|
38
|
+
<Attribute label='Event Time' value={formatEventTime(this.props.event_time)} />
|
|
39
|
+
<Attribute label='Event Version' value={this.props.event_version} />
|
|
40
|
+
<Attribute label='Checksum' value={this.props.checksum} />
|
|
41
|
+
<Attribute label='Payload'>
|
|
42
|
+
<JSONPretty className='json-pretty' json={this.props.payload} />
|
|
43
|
+
</Attribute>
|
|
44
|
+
</div>
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
}
|