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.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/.rspec +2 -0
- data/CHANGELOG.md +9 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +81 -0
- data/Rakefile +12 -0
- data/assets/index-52cbf3063583f3c09a4b-0.css +2 -0
- data/assets/index-52cbf3063583f3c09a4b-0.css.map +1 -0
- data/assets/index-52cbf3063583f3c09a4b-1.css +2 -0
- data/assets/index-52cbf3063583f3c09a4b-1.css.map +1 -0
- data/assets/index-52cbf3063583f3c09a4b.js +49 -0
- data/assets/index-52cbf3063583f3c09a4b.js.map +1 -0
- data/assets/index.html +12 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/circle.yml +29 -0
- data/frontend/.babelrc +3 -0
- data/frontend/.editorconfig +8 -0
- data/frontend/.eslintignore +2 -0
- data/frontend/.eslintrc +3 -0
- data/frontend/.npmignore +8 -0
- data/frontend/package.json +45 -0
- data/frontend/sagui.config.js +52 -0
- data/frontend/src/actions/event-details.js +42 -0
- data/frontend/src/actions/event-details.spec.js +71 -0
- data/frontend/src/actions/event-overview.js +11 -0
- data/frontend/src/actions/event-overview.spec.js +20 -0
- data/frontend/src/actions/event-retry.js +58 -0
- data/frontend/src/actions/event-retry.spec.js +117 -0
- data/frontend/src/actions/events-search.js +75 -0
- data/frontend/src/actions/events-search.spec.js +197 -0
- data/frontend/src/actions/flash-messages.js +16 -0
- data/frontend/src/actions/flash-messages.spec.js +23 -0
- data/frontend/src/actions/index.js +24 -0
- data/frontend/src/actions/search-input-filter.js +14 -0
- data/frontend/src/actions/search-input-filter.spec.js +20 -0
- data/frontend/src/api.js +46 -0
- data/frontend/src/components/event/error-message.js +19 -0
- data/frontend/src/components/event/error-message.scss +8 -0
- data/frontend/src/components/event/event.scss +3 -0
- data/frontend/src/components/event/index.js +77 -0
- data/frontend/src/components/event/index.spec.js +89 -0
- data/frontend/src/components/event/loading.js +16 -0
- data/frontend/src/components/event/style.js +16 -0
- data/frontend/src/components/event-overview/attribute.js +24 -0
- data/frontend/src/components/event-overview/event-overview.scss +28 -0
- data/frontend/src/components/event-overview/index.js +47 -0
- data/frontend/src/components/event-overview/index.spec.js +67 -0
- data/frontend/src/components/event-overview-dialog/event-overview-dialog.scss +15 -0
- data/frontend/src/components/event-overview-dialog/index.js +85 -0
- data/frontend/src/components/event-retry-dialog/index.js +72 -0
- data/frontend/src/components/events-list/events-list.scss +49 -0
- data/frontend/src/components/events-list/index.js +62 -0
- data/frontend/src/components/events-list/index.spec.js +59 -0
- data/frontend/src/components/flash-message/flash-message.scss +40 -0
- data/frontend/src/components/flash-message/index.js +45 -0
- data/frontend/src/components/flash-message/index.spec.js +59 -0
- data/frontend/src/components/flash-message-list/index.js +34 -0
- data/frontend/src/components/header/header.scss +29 -0
- data/frontend/src/components/header/index.js +44 -0
- data/frontend/src/components/search-input/index.js +104 -0
- data/frontend/src/components/search-input/index.spec.js +56 -0
- data/frontend/src/components/search-input/search-input.scss +7 -0
- data/frontend/src/configs.js +15 -0
- data/frontend/src/helpers.spec.js +2 -0
- data/frontend/src/index.html +12 -0
- data/frontend/src/index.js +26 -0
- data/frontend/src/index.scss +31 -0
- data/frontend/src/reducers/event-details.js +32 -0
- data/frontend/src/reducers/event-details.spec.js +54 -0
- data/frontend/src/reducers/events-filters.js +17 -0
- data/frontend/src/reducers/events-filters.spec.js +34 -0
- data/frontend/src/reducers/events.js +48 -0
- data/frontend/src/reducers/events.spec.js +95 -0
- data/frontend/src/reducers/flash-messages.js +14 -0
- data/frontend/src/reducers/flash-messages.spec.js +34 -0
- data/frontend/src/reducers/index.js +18 -0
- data/frontend/src/reducers/index.spec.js +20 -0
- data/frontend/src/reducers/xhr-status.js +75 -0
- data/frontend/src/reducers/xhr-status.spec.js +94 -0
- data/frontend/src/routes.js +20 -0
- data/frontend/src/store.js +15 -0
- data/frontend/src/views/event-details.js +50 -0
- data/frontend/src/views/events-search.js +112 -0
- data/frontend/src/views/events-search.scss +24 -0
- data/frontend/src/views/events-search.spec.js +96 -0
- data/frontend/src/views/layout.js +24 -0
- data/frontend/src/views/layout.scss +3 -0
- data/lib/phobos_checkpoint_ui/app.rb +11 -0
- data/lib/phobos_checkpoint_ui/static_app.rb +19 -0
- data/lib/phobos_checkpoint_ui/tasks.rb +20 -0
- data/lib/phobos_checkpoint_ui/version.rb +3 -0
- data/lib/phobos_checkpoint_ui.rb +10 -0
- data/phobos_checkpoint_ui.gemspec +53 -0
- data/screenshot1.png +0 -0
- data/screenshot2.png +0 -0
- 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,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'> ×</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
|
+
}
|