phobos_checkpoint_ui 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (99) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -0
  3. data/.rspec +2 -0
  4. data/CHANGELOG.md +9 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +81 -0
  8. data/Rakefile +12 -0
  9. data/assets/index-52cbf3063583f3c09a4b-0.css +2 -0
  10. data/assets/index-52cbf3063583f3c09a4b-0.css.map +1 -0
  11. data/assets/index-52cbf3063583f3c09a4b-1.css +2 -0
  12. data/assets/index-52cbf3063583f3c09a4b-1.css.map +1 -0
  13. data/assets/index-52cbf3063583f3c09a4b.js +49 -0
  14. data/assets/index-52cbf3063583f3c09a4b.js.map +1 -0
  15. data/assets/index.html +12 -0
  16. data/bin/console +14 -0
  17. data/bin/setup +8 -0
  18. data/circle.yml +29 -0
  19. data/frontend/.babelrc +3 -0
  20. data/frontend/.editorconfig +8 -0
  21. data/frontend/.eslintignore +2 -0
  22. data/frontend/.eslintrc +3 -0
  23. data/frontend/.npmignore +8 -0
  24. data/frontend/package.json +45 -0
  25. data/frontend/sagui.config.js +52 -0
  26. data/frontend/src/actions/event-details.js +42 -0
  27. data/frontend/src/actions/event-details.spec.js +71 -0
  28. data/frontend/src/actions/event-overview.js +11 -0
  29. data/frontend/src/actions/event-overview.spec.js +20 -0
  30. data/frontend/src/actions/event-retry.js +58 -0
  31. data/frontend/src/actions/event-retry.spec.js +117 -0
  32. data/frontend/src/actions/events-search.js +75 -0
  33. data/frontend/src/actions/events-search.spec.js +197 -0
  34. data/frontend/src/actions/flash-messages.js +16 -0
  35. data/frontend/src/actions/flash-messages.spec.js +23 -0
  36. data/frontend/src/actions/index.js +24 -0
  37. data/frontend/src/actions/search-input-filter.js +14 -0
  38. data/frontend/src/actions/search-input-filter.spec.js +20 -0
  39. data/frontend/src/api.js +46 -0
  40. data/frontend/src/components/event/error-message.js +19 -0
  41. data/frontend/src/components/event/error-message.scss +8 -0
  42. data/frontend/src/components/event/event.scss +3 -0
  43. data/frontend/src/components/event/index.js +77 -0
  44. data/frontend/src/components/event/index.spec.js +89 -0
  45. data/frontend/src/components/event/loading.js +16 -0
  46. data/frontend/src/components/event/style.js +16 -0
  47. data/frontend/src/components/event-overview/attribute.js +24 -0
  48. data/frontend/src/components/event-overview/event-overview.scss +28 -0
  49. data/frontend/src/components/event-overview/index.js +47 -0
  50. data/frontend/src/components/event-overview/index.spec.js +67 -0
  51. data/frontend/src/components/event-overview-dialog/event-overview-dialog.scss +15 -0
  52. data/frontend/src/components/event-overview-dialog/index.js +85 -0
  53. data/frontend/src/components/event-retry-dialog/index.js +72 -0
  54. data/frontend/src/components/events-list/events-list.scss +49 -0
  55. data/frontend/src/components/events-list/index.js +62 -0
  56. data/frontend/src/components/events-list/index.spec.js +59 -0
  57. data/frontend/src/components/flash-message/flash-message.scss +40 -0
  58. data/frontend/src/components/flash-message/index.js +45 -0
  59. data/frontend/src/components/flash-message/index.spec.js +59 -0
  60. data/frontend/src/components/flash-message-list/index.js +34 -0
  61. data/frontend/src/components/header/header.scss +29 -0
  62. data/frontend/src/components/header/index.js +44 -0
  63. data/frontend/src/components/search-input/index.js +104 -0
  64. data/frontend/src/components/search-input/index.spec.js +56 -0
  65. data/frontend/src/components/search-input/search-input.scss +7 -0
  66. data/frontend/src/configs.js +15 -0
  67. data/frontend/src/helpers.spec.js +2 -0
  68. data/frontend/src/index.html +12 -0
  69. data/frontend/src/index.js +26 -0
  70. data/frontend/src/index.scss +31 -0
  71. data/frontend/src/reducers/event-details.js +32 -0
  72. data/frontend/src/reducers/event-details.spec.js +54 -0
  73. data/frontend/src/reducers/events-filters.js +17 -0
  74. data/frontend/src/reducers/events-filters.spec.js +34 -0
  75. data/frontend/src/reducers/events.js +48 -0
  76. data/frontend/src/reducers/events.spec.js +95 -0
  77. data/frontend/src/reducers/flash-messages.js +14 -0
  78. data/frontend/src/reducers/flash-messages.spec.js +34 -0
  79. data/frontend/src/reducers/index.js +18 -0
  80. data/frontend/src/reducers/index.spec.js +20 -0
  81. data/frontend/src/reducers/xhr-status.js +75 -0
  82. data/frontend/src/reducers/xhr-status.spec.js +94 -0
  83. data/frontend/src/routes.js +20 -0
  84. data/frontend/src/store.js +15 -0
  85. data/frontend/src/views/event-details.js +50 -0
  86. data/frontend/src/views/events-search.js +112 -0
  87. data/frontend/src/views/events-search.scss +24 -0
  88. data/frontend/src/views/events-search.spec.js +96 -0
  89. data/frontend/src/views/layout.js +24 -0
  90. data/frontend/src/views/layout.scss +3 -0
  91. data/lib/phobos_checkpoint_ui/app.rb +11 -0
  92. data/lib/phobos_checkpoint_ui/static_app.rb +19 -0
  93. data/lib/phobos_checkpoint_ui/tasks.rb +20 -0
  94. data/lib/phobos_checkpoint_ui/version.rb +3 -0
  95. data/lib/phobos_checkpoint_ui.rb +10 -0
  96. data/phobos_checkpoint_ui.gemspec +53 -0
  97. data/screenshot1.png +0 -0
  98. data/screenshot2.png +0 -0
  99. 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
+ })
@@ -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,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,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
+ }