mirror of
https://github.com/weaveworks/scope.git
synced 2026-03-03 02:00:43 +00:00
Added service images widget
This commit is contained in:
@@ -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';
|
||||
@@ -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
client/app/scripts/components/cloud-feature.js
Normal file
34
client/app/scripts/components/cloud-feature.js
Normal file
@@ -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);
|
||||
@@ -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');
|
||||
@@ -230,6 +232,9 @@ class NodeDetails extends React.Component {
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
<CloudFeature>
|
||||
<NodeDetailsImageStatus name={details.label} metadata={details.metadata} />
|
||||
</CloudFeature>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
|
||||
@@ -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'
|
||||
}]
|
||||
}]
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -84,6 +84,7 @@ export const initialState = makeMap({
|
||||
viewport: makeMap(),
|
||||
websocketClosed: false,
|
||||
zoomCache: makeMap(),
|
||||
serviceImages: makeMap()
|
||||
});
|
||||
|
||||
function calcSelectType(topology) {
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user