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,67 @@
1
+ import React from 'react'
2
+ import jasmineEnzyme from 'jasmine-enzyme'
3
+ import { mount } from 'enzyme'
4
+ import EventOverview, { formatEventTime } from 'components/event-overview'
5
+ import getMuiTheme from 'material-ui/styles/getMuiTheme'
6
+ import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'
7
+
8
+ const mountComponent = (props) => mount(
9
+ <MuiThemeProvider muiTheme={getMuiTheme()}>
10
+ <EventOverview {...props} />
11
+ </MuiThemeProvider>
12
+ )
13
+
14
+ describe('<EventOverview />', () => {
15
+ let props, component
16
+
17
+ beforeEach(() => {
18
+ jasmineEnzyme()
19
+
20
+ props = {
21
+ id: 1,
22
+ topic: 'phobos.test',
23
+ group_id: 'phobos-checkpoint-consumer',
24
+ entity_id: 'a5dbd02d-bc40-6d15-b993-83a4825d94e6',
25
+ event_time: '2016-09-23T21:00:40.515Z',
26
+ event_type: 'order-placed',
27
+ event_version: 'v1',
28
+ checksum: '188773471ec0f898fd81d272760a027f',
29
+ payload: { data: { name: 'phobos' } }
30
+ }
31
+
32
+ component = mountComponent(props)
33
+ })
34
+
35
+ it('displays topic', () => {
36
+ expect(component.text()).toMatch(props.topic)
37
+ })
38
+
39
+ it('displays group_id', () => {
40
+ expect(component.text()).toMatch(props.group_id)
41
+ })
42
+
43
+ it('displays entity_id', () => {
44
+ expect(component.text()).toMatch(props.entity_id)
45
+ })
46
+
47
+ it('displays event_time formatted', () => {
48
+ expect(component.text()).toMatch(formatEventTime(props.event_time))
49
+ })
50
+
51
+ it('displays event_type', () => {
52
+ expect(component.text()).toMatch(props.event_type)
53
+ })
54
+
55
+ it('displays event_version', () => {
56
+ expect(component.text()).toMatch(props.event_version)
57
+ })
58
+
59
+ it('displays checksum', () => {
60
+ expect(component.text()).toMatch(props.checksum)
61
+ })
62
+
63
+ it('displays payload', () => {
64
+ const payloadFormatted = JSON.stringify(props.payload, null, ' ')
65
+ expect(component.text()).toMatch(payloadFormatted)
66
+ })
67
+ })
@@ -0,0 +1,15 @@
1
+ .event-overview-dialog {
2
+ .dialog-title {
3
+ text-decoration: none;
4
+ color: rgba(0, 0, 0, 0.87);
5
+
6
+ &:active,
7
+ &:visited {
8
+ color: inherit;
9
+ }
10
+
11
+ &:hover {
12
+ color: #666;
13
+ }
14
+ }
15
+ }
@@ -0,0 +1,85 @@
1
+ import React, { Component, PropTypes } from 'react'
2
+ import { connect } from 'react-redux'
3
+ import { Link } from 'react-router'
4
+
5
+ import { hideEventOverview } from 'actions/event-overview'
6
+ import { showEventRetry } from 'actions/event-retry'
7
+
8
+ import Dialog from 'material-ui/Dialog'
9
+ import RaisedButton from 'material-ui/RaisedButton'
10
+ import EventOverview from 'components/event-overview'
11
+
12
+ class OverviewDialog extends Component {
13
+ static get propTypes () {
14
+ return {
15
+ onHideOverview: PropTypes.func,
16
+ onShowRetry: PropTypes.func,
17
+ event: PropTypes.shape({
18
+ id: PropTypes.number,
19
+ group_id: PropTypes.string,
20
+ topic: PropTypes.string,
21
+ entity_id: PropTypes.string,
22
+ event_type: PropTypes.string,
23
+ event_time: PropTypes.string,
24
+ event_version: PropTypes.string,
25
+ checksum: PropTypes.string,
26
+ payload: PropTypes.object,
27
+
28
+ overviewVisible: PropTypes.bool
29
+ })
30
+ }
31
+ }
32
+
33
+ static get defaultProps () {
34
+ return {
35
+ event: {}
36
+ }
37
+ }
38
+
39
+ render () {
40
+ return (
41
+ <Dialog
42
+ modal={false}
43
+ autoScrollBodyContent
44
+ className='event-overview-dialog'
45
+ title={this.dialogTitle()}
46
+ open={!!this.props.event.overviewVisible}
47
+ onRequestClose={() => this.hideOverview()}
48
+ contentStyle={{maxWidth: '1024px'}}
49
+ bodyStyle={{maxWidth: '1024px'}}
50
+ actions={[
51
+ <RaisedButton
52
+ secondary
53
+ label='Retry'
54
+ onClick={() => this.showRetry()}/>
55
+ ]}>
56
+ <EventOverview {...this.props.event} />
57
+ </Dialog>
58
+ )
59
+ }
60
+
61
+ hideOverview () {
62
+ this.props.onHideOverview(this.props.event)
63
+ }
64
+
65
+ showRetry () {
66
+ this.props.onShowRetry(this.props.event)
67
+ }
68
+
69
+ dialogTitle () {
70
+ return (
71
+ <h3>
72
+ <Link className='dialog-title' to={`/events/${this.props.event.id}`}>
73
+ {`#${this.props.event.id}`}
74
+ </Link>
75
+ </h3>
76
+ )
77
+ }
78
+ }
79
+
80
+ export default connect(
81
+ (state, ownProps) => ownProps, {
82
+ onHideOverview: hideEventOverview,
83
+ onShowRetry: showEventRetry
84
+ }
85
+ )(OverviewDialog)
@@ -0,0 +1,72 @@
1
+ import React, { Component, PropTypes } from 'react'
2
+ import { connect } from 'react-redux'
3
+
4
+ import ErrorMessage from 'components/event/error-message'
5
+ import Loading from 'components/event/loading'
6
+
7
+ import { hideEventRetry, performEventRetry } from 'actions/event-retry'
8
+
9
+ import Dialog from 'material-ui/Dialog'
10
+ import RaisedButton from 'material-ui/RaisedButton'
11
+
12
+ class RetryDialog extends Component {
13
+ static get propTypes () {
14
+ return {
15
+ onHideRetry: PropTypes.func.isRequired,
16
+ onPerformRetry: PropTypes.func.isRequired,
17
+ isRetryingEvent: PropTypes.bool,
18
+ event: PropTypes.shape({
19
+ id: PropTypes.number,
20
+ retryVisible: PropTypes.bool
21
+ })
22
+ }
23
+ }
24
+
25
+ static get defaultProps () {
26
+ return {
27
+ event: {}
28
+ }
29
+ }
30
+
31
+ render () {
32
+ return (
33
+ <Dialog
34
+ modal={!!this.props.isRetryingEvent}
35
+ title='Are you sure?'
36
+ open={!!this.props.event.retryVisible}
37
+ bodyStyle={{maxWidth: '300px'}}
38
+ contentStyle={{maxWidth: '300px'}}
39
+ onRequestClose={() => this.hide()}
40
+ actions={[
41
+ <RaisedButton
42
+ primary
43
+ label='Retry'
44
+ onClick={() => this.performRetry()}/>
45
+ ]}>
46
+ <div style={{textAlign: 'center'}}>
47
+ <Loading visible={this.props.isRetryingEvent}/>
48
+ <ErrorMessage message={this.props.event.error}/>
49
+ </div>
50
+ </Dialog>
51
+ )
52
+ }
53
+
54
+ hide () {
55
+ this.props.onHideRetry(this.props.event)
56
+ }
57
+
58
+ performRetry () {
59
+ this.props.onPerformRetry(this.props.event)
60
+ }
61
+ }
62
+
63
+ const mapStateToProps = (state, ownProps) => (
64
+ Object.assign({
65
+ isRetryingEvent: state.xhrStatus.isRetryingEvent
66
+ }, ownProps)
67
+ )
68
+
69
+ export default connect(mapStateToProps, {
70
+ onHideRetry: hideEventRetry,
71
+ onPerformRetry: performEventRetry
72
+ })(RetryDialog)
@@ -0,0 +1,49 @@
1
+ .events-list {
2
+ display: flex;
3
+ flex-wrap: wrap;
4
+ flex-direction: column;
5
+ align-items: inherit;
6
+ position: relative;
7
+
8
+ .timeline {
9
+ padding-left: 40px;
10
+
11
+ &:before {
12
+ content: "";
13
+ position: absolute;
14
+ top: 0;
15
+ bottom: 0;
16
+ left: 15px;
17
+ display: block;
18
+ width: 2px;
19
+ background-color: #cdcdcd;
20
+ }
21
+
22
+ .dot {
23
+ border: 2px solid #868686;
24
+ border-radius: 50%;
25
+ width: 10px;
26
+ height: 10px;
27
+ display: inline-block;
28
+ margin: 0 10px 0 9px;
29
+ background-color: #e4e4e4;
30
+ z-index: 1;
31
+ }
32
+
33
+ .day-header {
34
+ display: inline-flex;
35
+ align-items: center;
36
+ color: #333;
37
+ font-family: Roboto;
38
+ margin: 10px 0 10px -40px;
39
+
40
+ &:first-child {
41
+ margin-top: 20px;
42
+ }
43
+ }
44
+
45
+ & > .event {
46
+ margin: 10px 10px;
47
+ }
48
+ }
49
+ }
@@ -0,0 +1,62 @@
1
+ import React, { Component, PropTypes } from 'react'
2
+ import moment from 'moment'
3
+ import Event from 'components/event'
4
+
5
+ const SECTION_DATE_FORMAT = 'MMM DD, YYYY'
6
+ const EVENT_TIME_FORMAT = 'YYYY-MM-DD'
7
+
8
+ export default class extends Component {
9
+ static get propTypes () {
10
+ return {
11
+ events: PropTypes.array
12
+ }
13
+ }
14
+
15
+ render () {
16
+ return (
17
+ <div className='events-list'>
18
+ <div className='timeline'>
19
+ {this.renderTimeline()}
20
+ </div>
21
+ </div>
22
+ )
23
+ }
24
+
25
+ renderTimeline () {
26
+ let day
27
+ let timeline = []
28
+
29
+ this.props.events.forEach((event) => {
30
+ const eventDay = formatEventDate(event.event_time)
31
+ if (day !== eventDay) {
32
+ day = eventDay
33
+ timeline.push(this.renderDayHeader(day))
34
+ }
35
+
36
+ timeline.push(
37
+ <Event key={`event-${event.id}`} event={event}/>
38
+ )
39
+ })
40
+
41
+ return timeline
42
+ }
43
+
44
+ renderDayHeader (day) {
45
+ return (
46
+ <div key={`day-header-${day}`} className='day-header'>
47
+ <span className='dot' />
48
+ {formatSectionDate(day)}
49
+ </div>
50
+ )
51
+ }
52
+ }
53
+
54
+ function formatEventDate (eventdate) {
55
+ if (!eventdate) return null
56
+ return moment(new Date(eventdate)).format(EVENT_TIME_FORMAT)
57
+ }
58
+
59
+ function formatSectionDate (date) {
60
+ if (!date) return null
61
+ return moment(new Date(date)).format(SECTION_DATE_FORMAT)
62
+ }
@@ -0,0 +1,59 @@
1
+ import React from 'react'
2
+ import jasmineEnzyme from 'jasmine-enzyme'
3
+ import { mount } from 'enzyme'
4
+ import EventsList from 'components/events-list'
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
+ <EventsList {...props} />
18
+ </MuiThemeProvider>
19
+ </Provider>
20
+ )
21
+
22
+ describe('<EventsList />', () => {
23
+ let store, props, component, event
24
+
25
+ beforeEach(() => {
26
+ jasmineEnzyme()
27
+
28
+ store = mockStore({
29
+ xhrStatus: {
30
+ isRetryingEvent: false
31
+ }
32
+ })
33
+
34
+ event = {
35
+ id: 1,
36
+ topic: 'phobos.test',
37
+ group_id: 'phobos-checkpoint-consumer',
38
+ entity_id: 'a5dbd02d-bc40-6d15-b993-83a4825d94e6',
39
+ event_time: '2016-09-23T21:00:40.515Z',
40
+ event_type: 'order-placed',
41
+ event_version: 'v1',
42
+ checksum: '188773471ec0f898fd81d272760a027f',
43
+ payload: { data: { name: 'phobos' } }
44
+ }
45
+
46
+ props = {
47
+ events: [
48
+ event,
49
+ Object.assign({}, event, { id: 2 })
50
+ ]
51
+ }
52
+
53
+ component = mountComponent(store, props)
54
+ })
55
+
56
+ it('renders the list of events', () => {
57
+ expect(component.find('.event').length).toEqual(2)
58
+ })
59
+ })
@@ -0,0 +1,40 @@
1
+ .flash-message {
2
+ position: fixed;
3
+ top: 0;
4
+ left: 0;
5
+ right: 0;
6
+
7
+ display: flex;
8
+ align-items: center;
9
+ justify-content: center;
10
+
11
+ z-index: 2000;
12
+ height: 64px;
13
+ color: #fff;
14
+ font-family: 'Roboto';
15
+ font-size: 20px;
16
+ max-width: 95%;
17
+
18
+ .close {
19
+ transition: all ease-in-out 200ms;
20
+ position: absolute;
21
+ right: 10px;
22
+ cursor: pointer;
23
+ font-weight: lighter;
24
+ padding: 4px 12px;
25
+ border-radius: 50%;
26
+
27
+ &:hover {
28
+ color: black;
29
+ background-color: white;
30
+ }
31
+ }
32
+
33
+ &.success {
34
+ background-color: #5bbd66;
35
+ }
36
+
37
+ &.error {
38
+ background-color: #ff5f5f;
39
+ }
40
+ }
@@ -0,0 +1,45 @@
1
+ import React, { Component, PropTypes } from 'react'
2
+
3
+ export const AUTO_CLOSE_TIMEOUT = 5 * 1000
4
+
5
+ export default class extends Component {
6
+ static get propTypes () {
7
+ return {
8
+ id: PropTypes.string.isRequired,
9
+ type: PropTypes.string.isRequired,
10
+ text: PropTypes.string.isRequired,
11
+ onClose: PropTypes.func.isRequired,
12
+ autoClose: PropTypes.bool
13
+ }
14
+ }
15
+
16
+ render () {
17
+ return (
18
+ <div className={`flash-message ${this.props.type}`}>
19
+ <span className='text'>{this.props.text}</span>
20
+ {this.closeButton()}
21
+ </div>
22
+ )
23
+ }
24
+
25
+ componentDidMount () {
26
+ if (this.props.autoClose) {
27
+ this.timeout = setTimeout(() => this.close(), AUTO_CLOSE_TIMEOUT)
28
+ }
29
+ }
30
+
31
+ componentWillUnmount () {
32
+ clearTimeout(this.timeout)
33
+ }
34
+
35
+ closeButton () {
36
+ return !this.props.autoClose
37
+ ? <span onClick={() => this.close()} className='close'> &times;</span>
38
+ : null
39
+ }
40
+
41
+ close () {
42
+ this.props.onClose &&
43
+ this.props.onClose(this.props.id)
44
+ }
45
+ }
@@ -0,0 +1,59 @@
1
+ import React from 'react'
2
+ import jasmineEnzyme from 'jasmine-enzyme'
3
+ import { mount } from 'enzyme'
4
+ import FlashMessage, { AUTO_CLOSE_TIMEOUT } from 'components/flash-message'
5
+ import getMuiTheme from 'material-ui/styles/getMuiTheme'
6
+ import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'
7
+
8
+ const mountComponent = (props) => mount(
9
+ <MuiThemeProvider muiTheme={getMuiTheme()}>
10
+ <FlashMessage {...props} />
11
+ </MuiThemeProvider>
12
+ )
13
+
14
+ describe('<FlashMessage />', () => {
15
+ let props, component, onClose
16
+
17
+ beforeEach(() => {
18
+ jasmineEnzyme()
19
+ jasmine.clock().install()
20
+ onClose = jasmine.createSpy('onClose')
21
+
22
+ props = {
23
+ id: '1',
24
+ type: 'success',
25
+ text: 'lorem ipsum',
26
+ onClose: onClose
27
+ }
28
+ component = mountComponent(props)
29
+ })
30
+
31
+ afterEach(() => {
32
+ jasmine.clock().uninstall()
33
+ })
34
+
35
+ it('displays the message with the correct type', () => {
36
+ expect(component.text()).toMatch(props.text)
37
+ expect(component.find(`.flash-message.${props.type}`).length).toEqual(1)
38
+ })
39
+
40
+ describe('when X is clicked', () => {
41
+ it('calls onClose', () => {
42
+ component.find('.close').simulate('click')
43
+ expect(onClose).toHaveBeenCalledWith(props.id)
44
+ })
45
+ })
46
+
47
+ describe('with autoClose = true', () => {
48
+ beforeEach(() => {
49
+ Object.assign(props, { autoClose: true })
50
+ component = mountComponent(props)
51
+ })
52
+
53
+ it('calls onClose after AUTO_CLOSE_TIMEOUT time', () => {
54
+ expect(onClose).not.toHaveBeenCalled()
55
+ jasmine.clock().tick(AUTO_CLOSE_TIMEOUT)
56
+ expect(onClose).toHaveBeenCalledWith(props.id)
57
+ })
58
+ })
59
+ })
@@ -0,0 +1,34 @@
1
+ import React, { Component, PropTypes } from 'react'
2
+ import { connect } from 'react-redux'
3
+ import { deleteFlashMessage } from 'actions/flash-messages'
4
+
5
+ import FlashMessage from 'components/flash-message'
6
+
7
+ class FlashMessageList extends Component {
8
+ static get propTypes () {
9
+ return {
10
+ deleteFlashMessage: PropTypes.func.isRequired
11
+ }
12
+ }
13
+
14
+ render () {
15
+ return (
16
+ <div className='flash-message-list'>
17
+ {
18
+ this.props.flashMessages.map((message) => (
19
+ <FlashMessage
20
+ key={message.id}
21
+ onClose={this.props.deleteFlashMessage}
22
+ {...message}/>
23
+ ))
24
+ }
25
+ </div>
26
+ )
27
+ }
28
+ }
29
+
30
+ export default connect(
31
+ (state) => ({flashMessages: state.flashMessages}), {
32
+ deleteFlashMessage
33
+ }
34
+ )(FlashMessageList)
@@ -0,0 +1,29 @@
1
+ .header {
2
+ display: inline-flex;
3
+ align-items: center;
4
+ text-decoration: none;
5
+
6
+ max-height: 64px;
7
+ box-sizing: border-box;
8
+
9
+ &:active,
10
+ &:visited,
11
+ &:hover {
12
+ color: white;
13
+ }
14
+
15
+ img {
16
+ height: 64px;
17
+ min-width: 54px;
18
+ margin: 5px 10px 5px 0;
19
+ }
20
+
21
+ .title {
22
+ transition: all ease-in-out 150ms;
23
+ color: white;
24
+
25
+ &:hover {
26
+ color: #999;
27
+ }
28
+ }
29
+ }
@@ -0,0 +1,44 @@
1
+ import React, { Component } from 'react'
2
+ import configs from 'configs'
3
+ import AppBar from 'material-ui/AppBar'
4
+ import Chip from 'material-ui/Chip'
5
+ import { Link } from 'react-router'
6
+
7
+ const DEFAULT_TITLE = 'Phobos Checkpoint'
8
+
9
+ const style = {
10
+ bar: {
11
+ backgroundColor: '#302e3a'
12
+ },
13
+ title: {
14
+ color: '#fff',
15
+ fontFamily: 'Roboto',
16
+ fontWeight: 'lighter'
17
+ },
18
+ envLabel: {
19
+ marginLeft: 30
20
+ }
21
+ }
22
+
23
+ export default class extends Component {
24
+ render () {
25
+ return (
26
+ <AppBar
27
+ title={this.logo()}
28
+ showMenuIconButton={false}
29
+ style={style.bar}
30
+ titleStyle={style.title}/>
31
+ )
32
+ }
33
+
34
+ logo () {
35
+ const { title, logo, env_label } = configs()
36
+ return (
37
+ <Link className='header' to='/'>
38
+ {logo && <img className='logo' src={logo} />}
39
+ <span className='title'>{title || DEFAULT_TITLE}</span>
40
+ <Chip className='env-label' style={style.envLabel}>{env_label}</Chip>
41
+ </Link>
42
+ )
43
+ }
44
+ }