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