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.
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
+ }