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,104 @@
1
+ import React, { Component, PropTypes } from 'react'
2
+ import { connect } from 'react-redux'
3
+
4
+ import {
5
+ changeSearchInputFilterType,
6
+ changeSearchInputFilterValue
7
+ } from 'actions/search-input-filter'
8
+
9
+ import { triggerSearch } from 'actions/events-search'
10
+
11
+ import SelectField from 'material-ui/SelectField'
12
+ import MenuItem from 'material-ui/MenuItem'
13
+ import TextField from 'material-ui/TextField'
14
+ import SearchSVG from 'material-ui/svg-icons/action/search'
15
+ import RaisedButton from 'material-ui/RaisedButton'
16
+ import CircularProgress from 'material-ui/CircularProgress'
17
+
18
+ import { DEFAULT_FILTER_TYPE } from 'reducers/events-filters'
19
+
20
+ export const ENTER_KEY = 13
21
+ const FILTER_TYPES = [
22
+ 'entity_id',
23
+ 'event_type',
24
+ 'group_id',
25
+ 'topic'
26
+ ]
27
+
28
+ export class SearchInput extends Component {
29
+ static get propTypes () {
30
+ return {
31
+ onSearch: PropTypes.func,
32
+ onChangeFilterType: PropTypes.func,
33
+ onChangeFilterValue: PropTypes.func,
34
+ isFetchingEvents: PropTypes.bool,
35
+
36
+ filterType: PropTypes.string,
37
+ filterValue: PropTypes.string
38
+ }
39
+ }
40
+
41
+ render () {
42
+ return (
43
+ <div className='search-input'>
44
+ <SelectField
45
+ style={{width: '140px'}}
46
+ value={this.props.filterType || DEFAULT_FILTER_TYPE}
47
+ onChange={(e, index, value) => this.changeFilterType(value)}>
48
+ {
49
+ FILTER_TYPES.map((type) => (
50
+ <MenuItem
51
+ key={type}
52
+ value={type}
53
+ primaryText={type} />
54
+ ))
55
+ }
56
+ </SelectField>
57
+ <TextField
58
+ name='filterValue'
59
+ style={{width: '100%'}}
60
+ onChange={(e) => this.changeFilterValue(e.target.value)}
61
+ onKeyDown={(e) => this.search(e)}
62
+ value={this.props.filterValue || ''}
63
+ hintText='Press ENTER to search' />
64
+ <RaisedButton
65
+ primary
66
+ onClick={() => this.search()}
67
+ style={{height: '50px', marginLeft: '20px'}}
68
+ icon={this.getButtonIcon()} />
69
+ </div>
70
+ )
71
+ }
72
+
73
+ changeFilterType (value) {
74
+ this.props.onChangeFilterType(value)
75
+ }
76
+
77
+ changeFilterValue (value) {
78
+ this.props.onChangeFilterValue(value)
79
+ }
80
+
81
+ search (e) {
82
+ if (e && e.keyCode === ENTER_KEY || !e) {
83
+ this.props.onSearch()
84
+ }
85
+ }
86
+
87
+ getButtonIcon () {
88
+ return this.props.isFetchingEvents
89
+ ? <CircularProgress size={0.4}/>
90
+ : <SearchSVG color={'#fff'} style={{width: 30, height: 50}}/>
91
+ }
92
+ }
93
+
94
+ const mapStateToProps = (state, ownProps) => (
95
+ Object.assign({
96
+ isFetchingEvents: state.xhrStatus.isFetchingEvents
97
+ }, ownProps)
98
+ )
99
+
100
+ export default connect(mapStateToProps, {
101
+ onSearch: triggerSearch,
102
+ onChangeFilterType: changeSearchInputFilterType,
103
+ onChangeFilterValue: changeSearchInputFilterValue
104
+ })(SearchInput)
@@ -0,0 +1,56 @@
1
+ import React from 'react'
2
+ import jasmineEnzyme from 'jasmine-enzyme'
3
+ import { mount } from 'enzyme'
4
+ import { SearchInput, ENTER_KEY } from 'components/search-input'
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
+ <SearchInput {...props} />
18
+ </MuiThemeProvider>
19
+ </Provider>
20
+ )
21
+
22
+ describe('<SearchInput />', () => {
23
+ let props, component, store
24
+
25
+ beforeEach(() => {
26
+ jasmineEnzyme()
27
+ store = mockStore({})
28
+
29
+ props = {
30
+ onSearch: jasmine.createSpy('onSearch'),
31
+ onChangeFilterType: jasmine.createSpy('onChangeFilterType'),
32
+ onChangeFilterValue: jasmine.createSpy('onChangeFilterValue'),
33
+ isFetchingEvents: false,
34
+
35
+ filterType: 'entity_id',
36
+ filterValue: '12345'
37
+ }
38
+
39
+ component = mountComponent(store, props)
40
+ })
41
+
42
+ it('calls onChangeFilterValue with the new value when type is changed', () => {
43
+ component.find('input').simulate('change', { target: { value: 'new-value' } })
44
+ expect(props.onChangeFilterValue).toHaveBeenCalledWith('new-value')
45
+ })
46
+
47
+ it('calls onSearch when ENTER is pressed in the textfield', () => {
48
+ component.find('input').simulate('keyDown', { keyCode: ENTER_KEY })
49
+ expect(props.onSearch).toHaveBeenCalled()
50
+ })
51
+
52
+ it('calls onSearch when the search button is pressed', () => {
53
+ component.find('button').simulate('click')
54
+ expect(props.onSearch).toHaveBeenCalled()
55
+ })
56
+ })
@@ -0,0 +1,7 @@
1
+ .search-input {
2
+ display: flex;
3
+ align-items: center;
4
+ background-color: #fff;
5
+ padding: 5px 30px;
6
+ margin: 0 10px;
7
+ }
@@ -0,0 +1,15 @@
1
+ import API from 'api'
2
+
3
+ let config
4
+
5
+ export function load () {
6
+ return API.Config
7
+ .load()
8
+ .then((response) => {
9
+ config = response.data
10
+ })
11
+ }
12
+
13
+ export default function get () {
14
+ return Object.assign({}, config)
15
+ }
@@ -0,0 +1,2 @@
1
+ import injectTapEventPlugin from 'react-tap-event-plugin'
2
+ injectTapEventPlugin()
@@ -0,0 +1,12 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>PhobosCheckpoint</title>
5
+ <meta charset="utf-8" />
6
+ </head>
7
+ <body>
8
+ <div id="root">
9
+ <div class='loading-boot'>Loading...</div>
10
+ </div>
11
+ </body>
12
+ </html>
@@ -0,0 +1,26 @@
1
+ import './index.scss'
2
+
3
+ import React from 'react'
4
+ import { render } from 'react-dom'
5
+ import { Provider } from 'react-redux'
6
+ import injectTapEventPlugin from 'react-tap-event-plugin'
7
+ injectTapEventPlugin()
8
+
9
+ import store from 'store'
10
+ import Routes from 'routes'
11
+ import { load } from 'configs'
12
+
13
+ const Root = (
14
+ <Provider store={store}>
15
+ {Routes}
16
+ </Provider>
17
+ )
18
+
19
+ const mountNode = document.getElementById('root')
20
+
21
+ load()
22
+ .then(() => render(Root, mountNode))
23
+ .catch(() => render(
24
+ <div className='load-error'>Failed to load configs</div>,
25
+ mountNode
26
+ ))
@@ -0,0 +1,31 @@
1
+ @import url('https://fonts.googleapis.com/css?family=Roboto:300,400,500');
2
+
3
+ html,
4
+ body {
5
+ margin: 0;
6
+ padding: 0;
7
+ }
8
+
9
+ html, body, #root {
10
+ height: 100%
11
+ }
12
+
13
+ body {
14
+ background-color: #ccc;
15
+ color: #d0d0d0;
16
+ }
17
+
18
+ .loading-boot,
19
+ .load-error {
20
+ margin: 0 auto;
21
+ color: black;
22
+ width: 100%;
23
+ height: 100%;
24
+ text-align: center;
25
+ font-size: 40px;
26
+ padding-top: 100px;
27
+ }
28
+
29
+ .load-error {
30
+ color: #ff5f5f;
31
+ }
@@ -0,0 +1,32 @@
1
+ import {
2
+ EVENT_SHOW_RETRY,
3
+ EVENT_HIDE_RETRY,
4
+ EVENT_HIDE_OVERVIEW,
5
+ RECEIVE_EVENT_DETAILS
6
+ } from 'actions'
7
+
8
+ function patchEvent (state, action, params) {
9
+ if (state.id === action.event.id) {
10
+ return Object.assign(state, params)
11
+ }
12
+ return state
13
+ }
14
+
15
+ export default (state = {}, action) => {
16
+ switch (action.type) {
17
+ case EVENT_SHOW_RETRY:
18
+ return patchEvent(state, action, { retryVisible: true })
19
+
20
+ case EVENT_HIDE_RETRY:
21
+ return patchEvent(state, action, { retryVisible: false })
22
+
23
+ case EVENT_HIDE_OVERVIEW:
24
+ return {}
25
+
26
+ case RECEIVE_EVENT_DETAILS:
27
+ return action.event
28
+
29
+ default:
30
+ return state
31
+ }
32
+ }
@@ -0,0 +1,54 @@
1
+ import {
2
+ EVENT_SHOW_RETRY,
3
+ EVENT_HIDE_RETRY,
4
+ EVENT_HIDE_OVERVIEW,
5
+ RECEIVE_EVENT_DETAILS
6
+ } from 'actions'
7
+
8
+ import reducer from 'reducers/event-details'
9
+
10
+ describe('reducers/events-details', () => {
11
+ describe('for EVENT_SHOW_RETRY', () => {
12
+ it('sets retryVisible to true', () => {
13
+ const currentState = { id: 1 }
14
+ const action = { type: EVENT_SHOW_RETRY, event: { id: 1 } }
15
+ const expectedState = { id: 1, retryVisible: true }
16
+ expect(reducer(currentState, action)).toEqual(expectedState)
17
+ })
18
+ })
19
+
20
+ describe('for EVENT_HIDE_RETRY', () => {
21
+ it('sets retryVisible to false', () => {
22
+ const currentState = { id: 1 }
23
+ const action = { type: EVENT_HIDE_RETRY, event: { id: 1 } }
24
+ const expectedState = { id: 1, retryVisible: false }
25
+ expect(reducer(currentState, action)).toEqual(expectedState)
26
+ })
27
+ })
28
+
29
+ describe('for EVENT_HIDE_OVERVIEW', () => {
30
+ it('erases the state', () => {
31
+ const currentState = { id: 1 }
32
+ const action = { type: EVENT_HIDE_OVERVIEW, event: { id: 1 } }
33
+ const expectedState = { }
34
+ expect(reducer(currentState, action)).toEqual(expectedState)
35
+ })
36
+ })
37
+
38
+ describe('for RECEIVE_EVENT_DETAILS', () => {
39
+ it('replaces the state', () => {
40
+ const currentState = { id: 1 }
41
+ const action = { type: RECEIVE_EVENT_DETAILS, event: { id: 2 } }
42
+ const expectedState = { id: 2 }
43
+ expect(reducer(currentState, action)).toEqual(expectedState)
44
+ })
45
+ })
46
+
47
+ describe('for default', () => {
48
+ it('returns the currentState', () => {
49
+ const currentState = { current: true }
50
+ const action = { type: 'another' }
51
+ expect(reducer(currentState, action)).toEqual(currentState)
52
+ })
53
+ })
54
+ })
@@ -0,0 +1,17 @@
1
+ import { SEARCH_INPUT_CHANGE_FILTER_TYPE, SEARCH_INPUT_CHANGE_FILTER_VALUE } from 'actions'
2
+
3
+ export const DEFAULT_FILTER_TYPE = 'entity_id'
4
+ const initialState = { type: DEFAULT_FILTER_TYPE }
5
+
6
+ export default (state = initialState, action) => {
7
+ switch (action.type) {
8
+ case SEARCH_INPUT_CHANGE_FILTER_TYPE:
9
+ return Object.assign({}, state, { type: action.filterType })
10
+
11
+ case SEARCH_INPUT_CHANGE_FILTER_VALUE:
12
+ return Object.assign({}, state, { value: action.filterValue })
13
+
14
+ default:
15
+ return state
16
+ }
17
+ }
@@ -0,0 +1,34 @@
1
+ import {
2
+ SEARCH_INPUT_CHANGE_FILTER_TYPE,
3
+ SEARCH_INPUT_CHANGE_FILTER_VALUE
4
+ } from 'actions'
5
+
6
+ import reducer from 'reducers/events-filters'
7
+
8
+ describe('reducers/events-filters', () => {
9
+ describe('for SEARCH_INPUT_CHANGE_FILTER_TYPE', () => {
10
+ it('sets type', () => {
11
+ const currentState = { type: 'foo' }
12
+ const action = { type: SEARCH_INPUT_CHANGE_FILTER_TYPE, filterType: 'bar' }
13
+ const expectedState = { type: 'bar' }
14
+ expect(reducer(currentState, action)).toEqual(expectedState)
15
+ })
16
+ })
17
+
18
+ describe('for SEARCH_INPUT_CHANGE_FILTER_VALUE', () => {
19
+ it('sets type', () => {
20
+ const currentState = { value: 'foo' }
21
+ const action = { type: SEARCH_INPUT_CHANGE_FILTER_VALUE, filterValue: 'bar' }
22
+ const expectedState = { value: 'bar' }
23
+ expect(reducer(currentState, action)).toEqual(expectedState)
24
+ })
25
+ })
26
+
27
+ describe('for default', () => {
28
+ it('returns the currentState', () => {
29
+ const currentState = { current: true }
30
+ const action = { type: 'another' }
31
+ expect(reducer(currentState, action)).toEqual(currentState)
32
+ })
33
+ })
34
+ })
@@ -0,0 +1,48 @@
1
+ import {
2
+ EVENT_SHOW_OVERVIEW,
3
+ EVENT_HIDE_OVERVIEW,
4
+ RECEIVE_SEARCH_RESULTS,
5
+ EVENT_SHOW_RETRY,
6
+ EVENT_HIDE_RETRY,
7
+ RECEIVE_EVENT_RETRY,
8
+ REQUEST_EVENT_RETRY_FAILED
9
+ } from 'actions'
10
+
11
+ function patchEvent (state, action, params) {
12
+ return state.map((event) => {
13
+ if (event.id === action.event.id) {
14
+ return Object.assign({}, event, params)
15
+ }
16
+ return event
17
+ })
18
+ }
19
+
20
+ export default (state = [], action) => {
21
+ switch (action.type) {
22
+ case EVENT_SHOW_OVERVIEW:
23
+ return patchEvent(state, action, { overviewVisible: true })
24
+
25
+ case EVENT_HIDE_OVERVIEW:
26
+ return patchEvent(state, action, { overviewVisible: false, error: null })
27
+
28
+ case RECEIVE_SEARCH_RESULTS:
29
+ return action.offset <= 0
30
+ ? action.events
31
+ : state.concat(action.events)
32
+
33
+ case EVENT_SHOW_RETRY:
34
+ return patchEvent(state, action, { retryVisible: true })
35
+
36
+ case EVENT_HIDE_RETRY:
37
+ return patchEvent(state, action, { retryVisible: false, error: null })
38
+
39
+ case RECEIVE_EVENT_RETRY:
40
+ return patchEvent(state, action, { acknowledged: action.acknowledged, error: null })
41
+
42
+ case REQUEST_EVENT_RETRY_FAILED:
43
+ return patchEvent(state, action, { error: action.error })
44
+
45
+ default:
46
+ return state
47
+ }
48
+ }
@@ -0,0 +1,95 @@
1
+ import {
2
+ EVENT_SHOW_OVERVIEW,
3
+ EVENT_HIDE_OVERVIEW,
4
+ RECEIVE_SEARCH_RESULTS,
5
+ EVENT_SHOW_RETRY,
6
+ EVENT_HIDE_RETRY,
7
+ RECEIVE_EVENT_RETRY,
8
+ REQUEST_EVENT_RETRY_FAILED
9
+ } from 'actions'
10
+
11
+ import reducer from 'reducers/events'
12
+
13
+ describe('reducers/events', () => {
14
+ describe('for EVENT_SHOW_OVERVIEW', () => {
15
+ it('sets overviewVisible for the specific event to true', () => {
16
+ const currentState = [{ id: 1 }, { id: 2 }, { id: 3 }]
17
+ const action = { type: EVENT_SHOW_OVERVIEW, event: { id: 2 } }
18
+ const expectedState = [{ id: 1 }, { id: 2, overviewVisible: true }, { id: 3 }]
19
+ expect(reducer(currentState, action)).toEqual(expectedState)
20
+ })
21
+ })
22
+
23
+ describe('for EVENT_HIDE_OVERVIEW', () => {
24
+ it('sets overviewVisible to false and erase errors for a specific', () => {
25
+ const currentState = [{ id: 1 }, { id: 2, overviewVisible: true }, { id: 3 }]
26
+ const action = { type: EVENT_HIDE_OVERVIEW, event: { id: 2 } }
27
+ const expectedState = [{ id: 1 }, { id: 2, overviewVisible: false, error: null }, { id: 3 }]
28
+ expect(reducer(currentState, action)).toEqual(expectedState)
29
+ })
30
+ })
31
+
32
+ describe('for RECEIVE_SEARCH_RESULTS', () => {
33
+ describe('when offset less or equal to 0', () => {
34
+ it('overrides events with action events', () => {
35
+ const currentState = [{ id: 1 }, { id: 2 }, { id: 3 }]
36
+ const action = { type: RECEIVE_SEARCH_RESULTS, offset: 0, events: [{ id: 4 }] }
37
+ const expectedState = [{ id: 4 }]
38
+ expect(reducer(currentState, action)).toEqual(expectedState)
39
+ })
40
+ })
41
+
42
+ describe('when offset is greater than 0', () => {
43
+ it('add the new events to state', () => {
44
+ const currentState = [{ id: 1 }, { id: 2 }, { id: 3 }]
45
+ const action = { type: RECEIVE_SEARCH_RESULTS, offset: 1, events: [{ id: 4 }] }
46
+ const expectedState = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }]
47
+ expect(reducer(currentState, action)).toEqual(expectedState)
48
+ })
49
+ })
50
+ })
51
+
52
+ describe('for EVENT_SHOW_RETRY', () => {
53
+ it('sets retryVisible for the specific event to true', () => {
54
+ const currentState = [{ id: 1 }, { id: 2 }, { id: 3 }]
55
+ const action = { type: EVENT_SHOW_RETRY, event: { id: 2 } }
56
+ const expectedState = [{ id: 1 }, { id: 2, retryVisible: true }, { id: 3 }]
57
+ expect(reducer(currentState, action)).toEqual(expectedState)
58
+ })
59
+ })
60
+
61
+ describe('for EVENT_HIDE_RETRY', () => {
62
+ it('sets retryVisible to false and erase errors for a specific', () => {
63
+ const currentState = [{ id: 1 }, { id: 2, retryVisible: true }, { id: 3 }]
64
+ const action = { type: EVENT_HIDE_RETRY, event: { id: 2 } }
65
+ const expectedState = [{ id: 1 }, { id: 2, retryVisible: false, error: null }, { id: 3 }]
66
+ expect(reducer(currentState, action)).toEqual(expectedState)
67
+ })
68
+ })
69
+
70
+ describe('for RECEIVE_EVENT_RETRY', () => {
71
+ it('sets acknowledged and erase errors for a specific', () => {
72
+ const currentState = [{ id: 1 }, { id: 2 }, { id: 3 }]
73
+ const action = { type: RECEIVE_EVENT_RETRY, event: { id: 2 }, acknowledged: true }
74
+ const expectedState = [{ id: 1 }, { id: 2, acknowledged: true, error: null }, { id: 3 }]
75
+ expect(reducer(currentState, action)).toEqual(expectedState)
76
+ })
77
+ })
78
+
79
+ describe('for REQUEST_EVENT_RETRY_FAILED', () => {
80
+ it('sets error for a specific', () => {
81
+ const currentState = [{ id: 1 }, { id: 2 }, { id: 3 }]
82
+ const action = { type: REQUEST_EVENT_RETRY_FAILED, event: { id: 2 }, error: 'some error' }
83
+ const expectedState = [{ id: 1 }, { id: 2, error: 'some error' }, { id: 3 }]
84
+ expect(reducer(currentState, action)).toEqual(expectedState)
85
+ })
86
+ })
87
+
88
+ describe('for default', () => {
89
+ it('returns the currentState', () => {
90
+ const currentState = { current: true }
91
+ const action = { type: 'another' }
92
+ expect(reducer(currentState, action)).toEqual(currentState)
93
+ })
94
+ })
95
+ })
@@ -0,0 +1,14 @@
1
+ import { ADD_FLASH_MESSAGE, DELETE_FLASH_MESSAGE } from 'actions'
2
+
3
+ export default (state = [], action) => {
4
+ switch (action.type) {
5
+ case ADD_FLASH_MESSAGE:
6
+ return [...state, action.message]
7
+
8
+ case DELETE_FLASH_MESSAGE:
9
+ return state.filter((message) => message.id !== action.id)
10
+
11
+ default:
12
+ return state
13
+ }
14
+ }
@@ -0,0 +1,34 @@
1
+ import {
2
+ ADD_FLASH_MESSAGE,
3
+ DELETE_FLASH_MESSAGE
4
+ } from 'actions'
5
+
6
+ import reducer from 'reducers/flash-messages'
7
+
8
+ describe('reducers/flash-messages', () => {
9
+ describe('for ADD_FLASH_MESSAGE', () => {
10
+ it('adds a new message', () => {
11
+ const currentState = []
12
+ const action = { type: ADD_FLASH_MESSAGE, message: { id: 1, type: 'success' } }
13
+ const expectedState = [action.message]
14
+ expect(reducer(currentState, action)).toEqual(expectedState)
15
+ })
16
+ })
17
+
18
+ describe('for DELETE_FLASH_MESSAGE', () => {
19
+ it('deletes the message by id', () => {
20
+ const currentState = [{ id: 1, type: 'success' }, { id: 2, type: 'error' }]
21
+ const action = { type: DELETE_FLASH_MESSAGE, id: 2 }
22
+ const expectedState = [{ id: 1, type: 'success' }]
23
+ expect(reducer(currentState, action)).toEqual(expectedState)
24
+ })
25
+ })
26
+
27
+ describe('for default', () => {
28
+ it('returns the currentState', () => {
29
+ const currentState = { current: true }
30
+ const action = { type: 'another' }
31
+ expect(reducer(currentState, action)).toEqual(currentState)
32
+ })
33
+ })
34
+ })
@@ -0,0 +1,18 @@
1
+ import { combineReducers } from 'redux'
2
+ import { routerReducer } from 'react-router-redux'
3
+
4
+ import events from 'reducers/events'
5
+ import eventDetails from 'reducers/event-details'
6
+ import eventsFilters from 'reducers/events-filters'
7
+ import xhrStatus from 'reducers/xhr-status'
8
+ import flashMessages from 'reducers/flash-messages'
9
+
10
+ export default combineReducers({
11
+ xhrStatus,
12
+ flashMessages,
13
+ eventsFilters,
14
+ eventDetails,
15
+ events,
16
+
17
+ routing: routerReducer
18
+ })
@@ -0,0 +1,20 @@
1
+ import reducer from 'reducers'
2
+
3
+ describe('reducers', () => {
4
+ it('without actions returns the initial state', () => {
5
+ expect(reducer(undefined, {})).toEqual({
6
+ xhrStatus: {
7
+ isFetchingEvents: false,
8
+ isRetryingEvent: false,
9
+ isFetchingEventDetails: false,
10
+ currentEventsOffset: 0,
11
+ lastEventsLoadSize: 0
12
+ },
13
+ flashMessages: [],
14
+ eventsFilters: { type: 'entity_id' },
15
+ eventDetails: {},
16
+ events: [],
17
+ routing: { locationBeforeTransitions: null }
18
+ })
19
+ })
20
+ })