Skip to content

Commit

Permalink
Added service images widget
Browse files Browse the repository at this point in the history
  • Loading branch information
jpellizzari committed May 3, 2017
1 parent 95660e3 commit 3ec7cc8
Show file tree
Hide file tree
Showing 10 changed files with 288 additions and 2 deletions.
24 changes: 24 additions & 0 deletions client/app/scripts/actions/app-actions.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import debug from 'debug';
import find from 'lodash/find';

import ActionTypes from '../constants/action-types';
import { saveGraph } from '../utils/file-utils';
Expand Down Expand Up @@ -792,3 +793,26 @@ export function shutdown() {
type: ActionTypes.SHUTDOWN
};
}

export function getImagesForService(orgId, serviceId) {
return (dispatch, getState, { api }) => {
dispatch({
type: ActionTypes.REQUEST_SERVICE_IMAGES,
serviceId
});

api.getFluxImages(orgId, serviceId)
.then((services) => {
dispatch({
type: ActionTypes.RECEIVE_SERVICE_IMAGES,
service: find(services, s => s.ID === serviceId),
serviceId
});
}, ({ errors }) => {
dispatch({
type: ActionTypes.RECEIVE_SERVICE_IMAGES,
errors
});
});
};
}
34 changes: 34 additions & 0 deletions client/app/scripts/components/cloud-feature.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import React from 'react';
import { connect } from 'react-redux';

class CloudFeature extends React.Component {
getChildContext() {
return {
store: this.context.serviceStore || this.context.store
};
}

render() {
if (process.env.WEAVE_CLOUD) {
return React.cloneElement(React.Children.only(this.props.children), {
params: this.context.router.params,
router: this.context.router
});
}

return null;
}
}

CloudFeature.contextTypes = {
store: React.PropTypes.object.isRequired,
router: React.PropTypes.object,
serviceStore: React.PropTypes.object
};

CloudFeature.childContextTypes = {
store: React.PropTypes.object,
router: React.PropTypes.object
};

export default connect()(CloudFeature);
5 changes: 5 additions & 0 deletions client/app/scripts/components/node-details.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import NodeDetailsInfo from './node-details/node-details-info';
import NodeDetailsRelatives from './node-details/node-details-relatives';
import NodeDetailsTable from './node-details/node-details-table';
import Warning from './warning';
import CloudFeature from './cloud-feature';
import NodeDetailsImageStatus from './node-details/node-details-image-status';


const log = debug('scope:node-details');
Expand Down Expand Up @@ -230,6 +232,9 @@ class NodeDetails extends React.Component {
}
return null;
})}
<CloudFeature>
<NodeDetailsImageStatus name={details.label} metadata={details.metadata} />
</CloudFeature>
</div>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import React from 'react';
import { connect } from 'react-redux';
import find from 'lodash/find';
import map from 'lodash/map';
import { CircularProgress } from 'weaveworks-ui-components';

import { getImagesForService } from '../../actions/app-actions';

const topologyWhitelist = ['services', 'deployments'];

function getNewImages(images, currentId) {
// Assume that the current image is always in the list of all available images.
// Should be a safe assumption...
const current = find(images, i => i.ID === currentId);
const timestamp = new Date(current.CreatedAt);
return find(images, i => timestamp < new Date(i.CreatedAt)) || [];
}

class NodeDetailsImageStatus extends React.PureComponent {
constructor(props, context) {
super(props, context);

this.handleServiceClick = this.handleServiceClick.bind(this);
}

componentDidMount() {
if (this.shouldRender() && this.props.serviceId) {
this.props.getImagesForService(this.props.params.orgId, this.props.serviceId);
}
}

handleServiceClick() {
const { router, serviceId, params } = this.props;
router.push(`/flux/${params.orgId}/services/${encodeURIComponent(serviceId)}`);
}

shouldRender() {
const { currentTopologyId } = this.props;
return currentTopologyId && topologyWhitelist.includes(currentTopologyId);
}

renderImages() {
const { errors, containers, isFetching } = this.props;
const error = !isFetching && errors;

if (isFetching) {
return (
<div className="progress-wrapper"><CircularProgress /></div>
);
}

if (error) {
return (
<p>Error: {JSON.stringify(map(errors, 'message'))}</p>
);
}

if (!containers) {
return 'No service images found';
}

return (
<div className="images">
{containers.map((container) => {
const statusText = getNewImages(container.Available, container.Current.ID).length > 0
? <span className="new-image">New image(s) available</span>
: 'Image up to date';

return (
<div key={container.Name} className="wrapper">
<div className="node-details-table-node-label">{container.Name}</div>
<div className="node-details-table-node-value">{statusText}</div>
</div>
);
})}
</div>
);
}

render() {
const { containers } = this.props;

if (!this.shouldRender()) {
return null;
}

return (
<div className="node-details-content-section image-status">
<div className="node-details-content-section-header">
Container Image Status
{containers &&
<div>
<a
onClick={this.handleServiceClick}
className="node-details-table-node-link">
View in Deploy
</a>
</div>
}

</div>
{this.renderImages()}
</div>
);
}
}

function mapStateToProps({ scope }, { metadata, name }) {
const namespace = find(metadata, d => d.id === 'kubernetes_namespace');
const serviceId = namespace ? `${namespace.value}/${name}` : null;
const { containers, isFetching, errors } = scope.getIn(['serviceImages', serviceId]) || {};

return {
isFetching,
errors,
currentTopologyId: scope.get('currentTopologyId'),
containers,
serviceId
};
}

export default connect(mapStateToProps, { getImagesForService })(NodeDetailsImageStatus);
4 changes: 3 additions & 1 deletion client/app/scripts/constants/action-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,9 @@ const ACTION_TYPES = [
'SET_VIEW_MODE',
'CHANGE_INSTANCE',
'TOGGLE_CONTRAST_MODE',
'SHUTDOWN'
'SHUTDOWN',
'REQUEST_SERVICE_IMAGES',
'RECEIVE_SERVICE_IMAGES'
];

export default zipObject(ACTION_TYPES, ACTION_TYPES);
33 changes: 33 additions & 0 deletions client/app/scripts/reducers/__tests__/root-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -678,4 +678,37 @@ describe('RootReducer', () => {
constructEdgeId('def456', 'abc123')
]);
});
it('receives images for a service', () => {
const action = {
type: ActionTypes.RECEIVE_SERVICE_IMAGES,
serviceId: 'cortex/configs',
service: {
ID: 'cortex/configs',
Containers: [{
Available: [{
ID: 'quay.io/weaveworks/cortex-configs:master-1ca6274a',
CreatedAt: '2017-04-26T13:50:13.284736173Z'
}],
Current: { ID: 'quay.io/weaveworks/cortex-configs:master-1ca6274a' },
Name: 'configs'
}]
}
};

const nextState = reducer(initialState, action);
expect(nextState.getIn(['serviceImages', 'cortex/configs'])).toEqual({
isFetching: false,
errors: undefined,
containers: [{
Name: 'configs',
Current: {
ID: 'quay.io/weaveworks/cortex-configs:master-1ca6274a'
},
Available: [{
ID: 'quay.io/weaveworks/cortex-configs:master-1ca6274a',
CreatedAt: '2017-04-26T13:50:13.284736173Z'
}]
}]
});
});
});
17 changes: 17 additions & 0 deletions client/app/scripts/reducers/root.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export const initialState = makeMap({
viewport: makeMap(),
websocketClosed: false,
zoomCache: makeMap(),
serviceImages: makeMap()
});

function calcSelectType(topology) {
Expand Down Expand Up @@ -741,6 +742,22 @@ export function rootReducer(state = initialState, action) {
return state.set('nodesLoaded', false);
}

case ActionTypes.REQUEST_SERVICE_IMAGES: {
return state.setIn(['serviceImages', action.serviceId], {
isFetching: true
});
}

case ActionTypes.RECEIVE_SERVICE_IMAGES: {
const { service, errors, serviceId } = action;

return state.setIn(['serviceImages', serviceId], {
isFetching: false,
containers: service ? service.Containers : null,
errors
});
}

default: {
return state;
}
Expand Down
47 changes: 47 additions & 0 deletions client/app/styles/_base.scss
Original file line number Diff line number Diff line change
Expand Up @@ -942,8 +942,55 @@

}
}

.image-status {

.progress-wrapper {
position: relative;
min-height: 35px;
}

.node-details-content-section-header {
display: flex;
justify-content: space-between;
line-height: 26px;
}

.images .wrapper{
display: flex;
justify-content: space-between;
}

.weave-circular-progress-wrapper {
position: absolute;
left: 50%;
}

.new-image {
color: $success-green;
}

a {
&:hover {
background-color: #f1f1f6;
cursor: pointer;
}
}

.node-details-table-node-link,
.node-details-table-node-label,
.node-details-table-node-value {
flex: 1;
font-size: 14px;
color: $text-color;
line-height: 24px;
text-transform: none;
}
}
}



.node-resources {
&-metric-box {
fill: rgba(150, 150, 150, 0.4);
Expand Down
1 change: 1 addition & 0 deletions client/app/styles/_variables.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ $weave-gray-blue: rgb(85,105,145);
$weave-blue: rgb(0,210,255);
$weave-orange: rgb(255,75,25);
$weave-charcoal-blue: rgb(50,50,75); // #32324B
$success-green: green;

$base-font: "Roboto", sans-serif;
$mono-font: "Menlo", "DejaVu Sans Mono", "Liberation Mono", monospace;
Expand Down
3 changes: 2 additions & 1 deletion client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@
"reselect-map": "1.0.0",
"whatwg-fetch": "2.0.1",
"react-addons-perf": "15.4.2",
"xterm": "2.2.3"
"xterm": "2.2.3",
"weaveworks-ui-components": "git+https://github.com/weaveworks/ui-components.git#v0.1.19"
},
"devDependencies": {
"autoprefixer": "6.5.3",
Expand Down

0 comments on commit 3ec7cc8

Please sign in to comment.