conjur-asset-ui-beta 1.5.0 → 1.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/TODO.md +0 -23
- data/app/build/js/app.js +72652 -50337
- data/app/package.json +2 -0
- data/app/src/actions.js +56 -3
- data/app/src/app.js +7 -3
- data/app/src/clients/graph.js +24 -0
- data/app/src/clients/members.js +3 -3
- data/app/src/components/audit/table.js +9 -0
- data/app/src/components/custom/view.js +26 -14
- data/app/src/components/dashboard/activity.js +24 -4
- data/app/src/components/generic/foldable_audit_section.js +17 -0
- data/app/src/components/generic/role_link.js +2 -1
- data/app/src/components/graph/graph.js +421 -0
- data/app/src/components/graph/graph.less +39 -0
- data/app/src/components/group/view.js +31 -14
- data/app/src/components/host/activity.js +24 -4
- data/app/src/components/host/executors.js +83 -0
- data/app/src/components/host/updaters.js +83 -0
- data/app/src/components/host/view.js +46 -14
- data/app/src/components/layer/view.js +30 -13
- data/app/src/components/policy/view.js +23 -14
- data/app/src/components/search/search.js +21 -7
- data/app/src/components/user/activity.js +25 -4
- data/app/src/components/user/view.js +30 -13
- data/app/src/components/variable/activity.js +25 -4
- data/app/src/components/variable/fetchers.js +1 -1
- data/app/src/components/variable/updaters.js +1 -1
- data/app/src/components/variable/view.js +24 -13
- data/app/src/constants.js +4 -2
- data/app/src/stores/graph_store.js +55 -0
- data/app/src/stores/host_store.js +12 -1
- data/app/src/stores/route_store.js +7 -5
- data/app/src/stores/search_store.js +11 -6
- data/conjur-asset-ui.gemspec +1 -1
- data/lib/conjur-asset-ui-version.rb +1 -1
- metadata +9 -3
@@ -0,0 +1,421 @@
|
|
1
|
+
'use strict';
|
2
|
+
|
3
|
+
require('./graph.less');
|
4
|
+
|
5
|
+
var React = require('react'),
|
6
|
+
Router = require('react-router'),
|
7
|
+
Fluxxor = require('fluxxor'),
|
8
|
+
FluxMixin = Fluxxor.FluxMixin(React),
|
9
|
+
StoreWatchMixin = Fluxxor.StoreWatchMixin,
|
10
|
+
dagre = require('dagre-d3'),
|
11
|
+
d3 = require('d3'),
|
12
|
+
forEach = require('lodash/collection/forEach');
|
13
|
+
|
14
|
+
var RoleLink = require('../generic/role_link');
|
15
|
+
|
16
|
+
// helpers
|
17
|
+
function _translate(x,y) {
|
18
|
+
return 'translate(' + x + ',' + y + ')';
|
19
|
+
}
|
20
|
+
|
21
|
+
function _findParent(elt, predicate) {
|
22
|
+
while (elt && !predicate(elt)) {
|
23
|
+
elt = elt.parentElement;
|
24
|
+
}
|
25
|
+
|
26
|
+
if (!elt) {
|
27
|
+
throw new Error('couldn\'t find parent');
|
28
|
+
}
|
29
|
+
|
30
|
+
return elt;
|
31
|
+
}
|
32
|
+
|
33
|
+
function _normalizeGraph(graph, invert) {
|
34
|
+
var result = [];
|
35
|
+
|
36
|
+
graph.forEach(function(edge) {
|
37
|
+
var parent = edge.parent || edge[0],
|
38
|
+
child = edge.child || edge[1];
|
39
|
+
|
40
|
+
var newEdge = invert ? {child: parent, parent: child} : {parent: parent, child: child};
|
41
|
+
|
42
|
+
result.push(newEdge);
|
43
|
+
});
|
44
|
+
|
45
|
+
return result;
|
46
|
+
}
|
47
|
+
|
48
|
+
function _isInternal(roleid) {
|
49
|
+
return roleid.indexOf('@') >= 0;
|
50
|
+
}
|
51
|
+
|
52
|
+
/**
|
53
|
+
* Displays a digraph representing a role tree. Usage:
|
54
|
+
*
|
55
|
+
* <RoleGraph margin={...} ref='roleGraph'/>
|
56
|
+
*
|
57
|
+
* ...
|
58
|
+
*
|
59
|
+
* // show a graph, which should be an array of {parent, child} objects.
|
60
|
+
* this.refs.roleGraph.setState({graph: res.graph})
|
61
|
+
*/
|
62
|
+
var RoleGraph = React.createClass({
|
63
|
+
mixins: [Router.State, Router.Navigation],
|
64
|
+
|
65
|
+
/*
|
66
|
+
* React Lifecycle methods
|
67
|
+
*/
|
68
|
+
render() {
|
69
|
+
return (
|
70
|
+
<div className="role-graph-container"></div>
|
71
|
+
);
|
72
|
+
},
|
73
|
+
|
74
|
+
getDefaultProps() {
|
75
|
+
return {
|
76
|
+
defaultOptions: {
|
77
|
+
margin: {
|
78
|
+
top: 20,
|
79
|
+
right: 20,
|
80
|
+
bottom: 20,
|
81
|
+
left: 20
|
82
|
+
}
|
83
|
+
}
|
84
|
+
};
|
85
|
+
},
|
86
|
+
|
87
|
+
getInitialState() {
|
88
|
+
return {
|
89
|
+
ready: false
|
90
|
+
};
|
91
|
+
},
|
92
|
+
|
93
|
+
componentDidMount() {
|
94
|
+
this.ctx = {};
|
95
|
+
this.el = this.getDOMNode();
|
96
|
+
this._propsToState(this.props);
|
97
|
+
},
|
98
|
+
|
99
|
+
componentWillReceiveProps(nextProps) {
|
100
|
+
this._propsToState(nextProps);
|
101
|
+
},
|
102
|
+
|
103
|
+
componentDidUpdate() {
|
104
|
+
if (this.state.ready === false) {
|
105
|
+
return;
|
106
|
+
}
|
107
|
+
|
108
|
+
if (this.ctx.svg === undefined) {
|
109
|
+
this._create();
|
110
|
+
}
|
111
|
+
|
112
|
+
if (this.state.graph !== undefined) {
|
113
|
+
this._update();
|
114
|
+
}
|
115
|
+
},
|
116
|
+
|
117
|
+
/**
|
118
|
+
* Internal methods
|
119
|
+
*/
|
120
|
+
|
121
|
+
/**
|
122
|
+
* Create non-data bound components. May be called whenever state.ready is true
|
123
|
+
* @private
|
124
|
+
*/
|
125
|
+
_create() {
|
126
|
+
this.ctx.svg = d3.select(this.el)
|
127
|
+
.append('svg')
|
128
|
+
.attr('class', 'role-graph-svg');
|
129
|
+
this.ctx.inner = this.ctx.svg.append('g');
|
130
|
+
},
|
131
|
+
|
132
|
+
/**
|
133
|
+
* Render the graph represented by this.state.graph. This may be
|
134
|
+
* called whenever this.state.graph is defined. this.state.graph
|
135
|
+
* must be an array of {child:'child-id', parent:'parent-id'}
|
136
|
+
* objects.
|
137
|
+
*
|
138
|
+
* @private
|
139
|
+
*/
|
140
|
+
_update() {
|
141
|
+
var svg = this.ctx.svg;
|
142
|
+
var graph = this._prepareD3Graph();
|
143
|
+
var inner = this.ctx.inner;
|
144
|
+
var render = new dagre.render();
|
145
|
+
render(inner, graph);
|
146
|
+
|
147
|
+
// Move and resize the SVG to match the width and height of
|
148
|
+
// the graph. Here, we use this.state.margin to compute the
|
149
|
+
// position of the <g/> element containing the graph within the
|
150
|
+
// root <svg> element, and the size of both the container and
|
151
|
+
// the svg.
|
152
|
+
|
153
|
+
var margin = this.state.margin;
|
154
|
+
var gWidth = graph.graph().width,
|
155
|
+
gHeight = graph.graph().height;
|
156
|
+
|
157
|
+
// total width/height are gWidth/gHeight + margin
|
158
|
+
var totalHeight = gHeight + margin.top + margin.bottom;
|
159
|
+
var totalWidth = gWidth + margin.left + margin.right;
|
160
|
+
|
161
|
+
var svgSize = this._computeSvgSize(gWidth, gHeight);
|
162
|
+
|
163
|
+
// resize the svg
|
164
|
+
svg.attr('height', svgSize.height)
|
165
|
+
.attr('width', svgSize.width);
|
166
|
+
|
167
|
+
// resize the inner <g> and translate it to the top left point of the
|
168
|
+
// margin.
|
169
|
+
inner.attr('height', gHeight)
|
170
|
+
.attr('width', gWidth)
|
171
|
+
.attr('transform',_translate(margin.left, margin.top));
|
172
|
+
|
173
|
+
// Set up zoom support
|
174
|
+
var zoom = d3.behavior.zoom().on('zoom', function() {
|
175
|
+
inner.attr('transform', 'translate(' + d3.event.translate + ')' +
|
176
|
+
'scale(' + d3.event.scale + ')');
|
177
|
+
});
|
178
|
+
|
179
|
+
// scale to initial scale
|
180
|
+
zoom.scale(svgSize.scale)
|
181
|
+
.translate([(svgSize.width - gWidth * svgSize.scale) / 2, (svgSize.height - gHeight * svgSize.scale) / 2]) // center it
|
182
|
+
.event(svg);
|
183
|
+
svg.call(zoom);
|
184
|
+
},
|
185
|
+
|
186
|
+
/**
|
187
|
+
* Return width and height attributes that will fit in our element (or fit the
|
188
|
+
* graph if our element isn't specifying a width and height), and a scale
|
189
|
+
* to apply to the graph to fit it in the svg, based on the given graph
|
190
|
+
* width and height.
|
191
|
+
* @param graphWidth Width, in pixels, of the graph
|
192
|
+
* @param graphHeight Height, in pixels, of the graph
|
193
|
+
* @return {{width:number, height: number, scale: number}}
|
194
|
+
*
|
195
|
+
* @private
|
196
|
+
*/
|
197
|
+
|
198
|
+
_computeSvgSize(graphWidth, graphHeight) {
|
199
|
+
var eltWidth = +this.el.offsetWidth,
|
200
|
+
eltHeight = +this.el.offsetHeight,
|
201
|
+
width = graphWidth,
|
202
|
+
height = graphHeight;
|
203
|
+
|
204
|
+
if (!isNaN(eltWidth) && eltWidth !== 0) {
|
205
|
+
width = Math.min(graphWidth, eltWidth);
|
206
|
+
}
|
207
|
+
|
208
|
+
if (!isNaN(eltHeight) && eltHeight !== 0) {
|
209
|
+
height = Math.min(graphHeight, eltHeight);
|
210
|
+
}
|
211
|
+
|
212
|
+
if (this.props.width) {
|
213
|
+
width = Math.max(this.props.width, width);
|
214
|
+
}
|
215
|
+
|
216
|
+
if (this.props.height) {
|
217
|
+
height = Math.max(this.props.height, height);
|
218
|
+
}
|
219
|
+
|
220
|
+
var scale = Math.min(width, height) / Math.max(graphWidth, graphHeight);
|
221
|
+
|
222
|
+
if (scale > 1) {
|
223
|
+
scale *= .9; // leave some room at the edges when the graph is smaller than the element.
|
224
|
+
}
|
225
|
+
|
226
|
+
return {width: width, height: height, scale: scale};
|
227
|
+
},
|
228
|
+
|
229
|
+
// creates an HTML role-link for the role.
|
230
|
+
_createRoleLabel(roleid) {
|
231
|
+
return React.withContext(this.context, function() {
|
232
|
+
var roleLink = <RoleLink id={roleid} simple={true} />;
|
233
|
+
return React.renderComponentToString(roleLink);
|
234
|
+
});
|
235
|
+
},
|
236
|
+
|
237
|
+
/**
|
238
|
+
* Build a graphlib Graph object from the graph data returned by
|
239
|
+
* the conjur api.
|
240
|
+
*
|
241
|
+
* May be called whenever this.state.graph is defined.
|
242
|
+
* @returns {degreD3.graphlib.Graph}
|
243
|
+
* @private
|
244
|
+
*/
|
245
|
+
_prepareD3Graph() {
|
246
|
+
var self = this,
|
247
|
+
gr = this.state.graph,
|
248
|
+
invert = this.state.invertGraph == undefined ? true : this.state.invertGraph;
|
249
|
+
|
250
|
+
gr = _normalizeGraph(gr, invert);
|
251
|
+
|
252
|
+
// remove internal roles from the graph,
|
253
|
+
// unless the role being shown is internal (presently this is
|
254
|
+
// impossible in the UI, but we might as well check in case this changes.
|
255
|
+
if (!_isInternal(this.state.roleid)) {
|
256
|
+
gr = gr.filter(function(edge) {
|
257
|
+
return !(_isInternal(edge.parent) || _isInternal(edge.child));
|
258
|
+
});
|
259
|
+
}
|
260
|
+
|
261
|
+
// graph properties go here
|
262
|
+
var graph = this.graph = new dagre.graphlib.Graph().setGraph({});
|
263
|
+
|
264
|
+
var nodeSet = {},
|
265
|
+
edgeSet = {};
|
266
|
+
|
267
|
+
function put(members, id) {
|
268
|
+
if (members[id]) {
|
269
|
+
return false;
|
270
|
+
}
|
271
|
+
|
272
|
+
members[id] = true;
|
273
|
+
return true;
|
274
|
+
}
|
275
|
+
|
276
|
+
function addNode(node) {
|
277
|
+
if (put(nodeSet, node)) {
|
278
|
+
graph.setNode(node, {
|
279
|
+
label: self._createRoleLabel(node),
|
280
|
+
labelType: 'html'
|
281
|
+
});
|
282
|
+
}
|
283
|
+
}
|
284
|
+
|
285
|
+
function addEdge(edge) {
|
286
|
+
var parent = edge.parent,
|
287
|
+
child = edge.child,
|
288
|
+
edgeId = parent + '' + child; // poor mans hash code :-(
|
289
|
+
|
290
|
+
addNode(parent);
|
291
|
+
addNode(child);
|
292
|
+
|
293
|
+
if (put(edgeSet, edgeId)) {
|
294
|
+
// edge properties defined here
|
295
|
+
graph.setEdge(parent, child,{});
|
296
|
+
}
|
297
|
+
}
|
298
|
+
|
299
|
+
forEach(gr, addEdge);
|
300
|
+
|
301
|
+
var selfNode = graph.node(this.state.roleid);
|
302
|
+
|
303
|
+
if (selfNode !== undefined) {
|
304
|
+
selfNode['class'] = 'current-role';
|
305
|
+
}
|
306
|
+
|
307
|
+
return graph;
|
308
|
+
},
|
309
|
+
|
310
|
+
/**
|
311
|
+
* Transfer some props to our state, and determine whether we're ready to
|
312
|
+
* call _create.
|
313
|
+
* @param props contains a 'margin' object.
|
314
|
+
* @private
|
315
|
+
*/
|
316
|
+
_propsToState(props) {
|
317
|
+
var margin = props.margin ? props.margin : this.props.defaultOptions.margin;
|
318
|
+
|
319
|
+
var state = {
|
320
|
+
ready: margin !== undefined && this.props.roleid !== undefined,
|
321
|
+
margin: margin,
|
322
|
+
invertGraph: this.props.invertGraph,
|
323
|
+
roleid: this.props.roleid
|
324
|
+
};
|
325
|
+
|
326
|
+
if (props.graph !== undefined) {
|
327
|
+
state.graph = props.graph;
|
328
|
+
}
|
329
|
+
|
330
|
+
this.setState(state);
|
331
|
+
}
|
332
|
+
});
|
333
|
+
|
334
|
+
module.exports = React.createClass({
|
335
|
+
displayName: 'RoleGraph',
|
336
|
+
|
337
|
+
mixins: [FluxMixin, StoreWatchMixin('graph')],
|
338
|
+
|
339
|
+
getStateFromFlux() {
|
340
|
+
var flux = this.getFlux(),
|
341
|
+
graph = flux.store('graph').getGraph(this.props.kind, this.props.id);
|
342
|
+
|
343
|
+
return graph;
|
344
|
+
},
|
345
|
+
|
346
|
+
render() {
|
347
|
+
if (this.state.loading) {
|
348
|
+
return (
|
349
|
+
<div className="rg-container">
|
350
|
+
<h2>Role Graph</h2>
|
351
|
+
Loading
|
352
|
+
</div>
|
353
|
+
);
|
354
|
+
}
|
355
|
+
|
356
|
+
if (this.state.data.length === 0) {
|
357
|
+
return (
|
358
|
+
<div className="rg-container">
|
359
|
+
<h2>Role Graph</h2>
|
360
|
+
No data exists
|
361
|
+
</div>
|
362
|
+
);
|
363
|
+
}
|
364
|
+
|
365
|
+
return this._renderGraph();
|
366
|
+
},
|
367
|
+
|
368
|
+
componentDidMount() {
|
369
|
+
var actions = this.getFlux().actions;
|
370
|
+
|
371
|
+
actions.graph.load(this.props.kind, this.props.id);
|
372
|
+
},
|
373
|
+
|
374
|
+
_renderGraph() {
|
375
|
+
var size = this._computeGraphSize(),
|
376
|
+
invert = this.props.invertGraph === undefined ? true : this.props.invertGraph,
|
377
|
+
roleid = conjur.app.configuration.account + ':' + this.props.kind + ':' + this.props.id;
|
378
|
+
|
379
|
+
return (
|
380
|
+
<div className="rg-container loaded">
|
381
|
+
<h2>Role Graph</h2>
|
382
|
+
<RoleGraph graph={this.state.data}
|
383
|
+
width={size.width}
|
384
|
+
height={size.height}
|
385
|
+
roleid={roleid}
|
386
|
+
invertGraph={invert} />
|
387
|
+
</div>
|
388
|
+
);
|
389
|
+
},
|
390
|
+
|
391
|
+
_computeGraphSize() {
|
392
|
+
var el = this.getDOMNode(),
|
393
|
+
width = this.props.width,
|
394
|
+
height = this.props.height;
|
395
|
+
|
396
|
+
if (isNaN(width)) {
|
397
|
+
width = +el.offsetWidth;
|
398
|
+
}
|
399
|
+
|
400
|
+
if (isNaN(height)) {
|
401
|
+
height = +el.offsetHeight;
|
402
|
+
}
|
403
|
+
|
404
|
+
if (isNaN(width) || isNaN(height)) {
|
405
|
+
throw new Error('can\'t determine width and height. ' +
|
406
|
+
'Perhaps you should set width and height props on your RoleGraphContainer.');
|
407
|
+
}
|
408
|
+
|
409
|
+
// we can set height ourselves but width should be computed from the parent.
|
410
|
+
if (width == 0) {
|
411
|
+
width = _findParent(el, function(e) {
|
412
|
+
return e.offsetWidth !== 0;
|
413
|
+
}).offsetWidth; // _findParent is a partial function, if no such parent exists it will throw an Error.
|
414
|
+
}
|
415
|
+
|
416
|
+
return {
|
417
|
+
width: width,
|
418
|
+
height: height
|
419
|
+
};
|
420
|
+
}
|
421
|
+
});
|
@@ -0,0 +1,39 @@
|
|
1
|
+
// MUST BE done in BEM style!!
|
2
|
+
|
3
|
+
.role-graph {
|
4
|
+
height: 300px;
|
5
|
+
}
|
6
|
+
|
7
|
+
.role-graph-svg {
|
8
|
+
border: 1px solid #CCC;
|
9
|
+
}
|
10
|
+
|
11
|
+
.rg-container {
|
12
|
+
width: 100%;
|
13
|
+
}
|
14
|
+
|
15
|
+
.rg-container .loaded {
|
16
|
+
min-height: 500px;
|
17
|
+
}
|
18
|
+
|
19
|
+
/** SVG role graph styles **/
|
20
|
+
.node rect {
|
21
|
+
stroke: #333;
|
22
|
+
fill: #fff;
|
23
|
+
}
|
24
|
+
|
25
|
+
svg .label {
|
26
|
+
font-weight: normal;
|
27
|
+
height: auto;
|
28
|
+
width: auto;
|
29
|
+
}
|
30
|
+
|
31
|
+
.edgePath path {
|
32
|
+
stroke: #333;
|
33
|
+
fill: #333;
|
34
|
+
stroke-width: 1.5px;
|
35
|
+
}
|
36
|
+
|
37
|
+
g.current-role > rect {
|
38
|
+
fill: #7F7;
|
39
|
+
}
|
@@ -3,8 +3,7 @@
|
|
3
3
|
var React = require('react'),
|
4
4
|
Fluxxor = require('fluxxor'),
|
5
5
|
FluxMixin = Fluxxor.FluxMixin(React),
|
6
|
-
StoreWatchMixin = Fluxxor.StoreWatchMixin
|
7
|
-
Router = require('react-router');
|
6
|
+
StoreWatchMixin = Fluxxor.StoreWatchMixin;
|
8
7
|
|
9
8
|
var TabbedArea = require('react-bootstrap/lib/TabbedArea'),
|
10
9
|
TabPane = require('react-bootstrap/lib/TabPane');
|
@@ -12,6 +11,7 @@ var TabbedArea = require('react-bootstrap/lib/TabbedArea'),
|
|
12
11
|
var TabMixin = require('../generic/tab_mixin'),
|
13
12
|
Breadcrumbs = require('../generic/breadcrumbs'),
|
14
13
|
RoleLink = require('../generic/role_link'),
|
14
|
+
RoleGraph = require('../graph/graph'),
|
15
15
|
utils = require('../../utils');
|
16
16
|
|
17
17
|
var AuditTable = require('../audit/table');
|
@@ -22,22 +22,45 @@ var compact = require('lodash/array/compact'),
|
|
22
22
|
module.exports = React.createClass({
|
23
23
|
displayName: 'GroupView',
|
24
24
|
|
25
|
-
mixins: [FluxMixin, StoreWatchMixin('audit', 'group', 'resources'
|
25
|
+
mixins: [FluxMixin, StoreWatchMixin('audit', 'group', 'resources', 'route'), TabMixin],
|
26
|
+
|
27
|
+
askForNewData(state) {
|
28
|
+
var actions = this.getFlux().actions;
|
29
|
+
|
30
|
+
actions.audit.loadForRole('group', state.id);
|
31
|
+
actions.audit.loadForResource('group', state.id);
|
32
|
+
actions.group.load(state.id);
|
33
|
+
actions.resources.loadOne('group', state.id);
|
34
|
+
},
|
35
|
+
|
36
|
+
componentDidMount() {
|
37
|
+
this.askForNewData(this.state);
|
38
|
+
},
|
39
|
+
|
40
|
+
componentWillUpdate(nextProps, nextState) {
|
41
|
+
if (this.state.id !== nextState.id) {
|
42
|
+
this.askForNewData(nextState);
|
43
|
+
}
|
44
|
+
|
45
|
+
return true;
|
46
|
+
},
|
26
47
|
|
27
48
|
getStateFromFlux() {
|
28
49
|
var flux = this.getFlux(),
|
29
|
-
id =
|
50
|
+
id = flux.store('route').getParam('id'),
|
30
51
|
audit = flux.store('audit').getEventFor('group', id),
|
31
52
|
data = flux.store('group').getData(),
|
32
53
|
resource = flux.store('resources').getResource('group', id);
|
33
54
|
|
34
55
|
var ret = {
|
56
|
+
id: id,
|
35
57
|
loading: data.loading,
|
36
58
|
group: data.group,
|
37
59
|
owned: data.owned,
|
38
60
|
roles: data.roles,
|
39
61
|
owner: resource.data.owner,
|
40
62
|
annotations: resource.data.annotations,
|
63
|
+
resource: resource.data,
|
41
64
|
members: data.members,
|
42
65
|
resources: data.resources,
|
43
66
|
audit: audit.data
|
@@ -121,17 +144,11 @@ module.exports = React.createClass({
|
|
121
144
|
<TabbedArea defaultActiveKey="overview">
|
122
145
|
{tabs}
|
123
146
|
</TabbedArea>
|
147
|
+
<hr />
|
148
|
+
<RoleGraph height="400"
|
149
|
+
kind="group"
|
150
|
+
id={this.state.id} />
|
124
151
|
</div>
|
125
152
|
);
|
126
|
-
},
|
127
|
-
|
128
|
-
componentDidMount() {
|
129
|
-
var actions = this.getFlux().actions,
|
130
|
-
id = unescape(this.getParams().id);
|
131
|
-
|
132
|
-
actions.audit.loadForRole('group', id);
|
133
|
-
actions.audit.loadForResource('group', id);
|
134
|
-
actions.group.load(id);
|
135
|
-
actions.resources.loadOne('group', id);
|
136
153
|
}
|
137
154
|
});
|
@@ -76,17 +76,37 @@ module.exports = React.createClass({
|
|
76
76
|
|
77
77
|
return null;
|
78
78
|
},
|
79
|
+
|
80
|
+
getInitialState() {
|
81
|
+
return {showActivity: false};
|
82
|
+
},
|
83
|
+
|
84
|
+
toggleActivity(e) {
|
85
|
+
e.preventDefault();
|
86
|
+
|
87
|
+
this.setState({showActivity: !this.state.showActivity});
|
88
|
+
},
|
79
89
|
|
80
90
|
render() {
|
81
91
|
var data = this.getData(this.props.audit);
|
92
|
+
var activity_body = [];
|
93
|
+
|
94
|
+
var msg = this.state.showActivity ? 'Hide' : 'Show graph';
|
95
|
+
|
96
|
+
if (this.state.showActivity) {
|
97
|
+
activity_body.push(
|
98
|
+
<div className="b-host-activity__graph">
|
99
|
+
<Chart options={this.props.options}
|
100
|
+
data={data} />
|
101
|
+
</div>
|
102
|
+
);
|
103
|
+
}
|
82
104
|
|
83
105
|
return (
|
84
106
|
<div className="b-host-activity">
|
85
107
|
<h2>Activity<Refresh show={this.props.isLoading} /></h2>
|
86
|
-
<
|
87
|
-
|
88
|
-
data={data} />
|
89
|
-
</div>
|
108
|
+
<a href="#" onClick={this.toggleActivity}>{msg}</a>
|
109
|
+
{activity_body}
|
90
110
|
</div>
|
91
111
|
);
|
92
112
|
}
|
@@ -0,0 +1,83 @@
|
|
1
|
+
'use strict';
|
2
|
+
|
3
|
+
var React = require('react'),
|
4
|
+
take = require('lodash/array/take');
|
5
|
+
|
6
|
+
var RoleLink = require('../generic/role_link'),
|
7
|
+
Refresh = require('../refresh/refresh');
|
8
|
+
|
9
|
+
module.exports = React.createClass({
|
10
|
+
displayName: 'HostExecutors',
|
11
|
+
|
12
|
+
propTypes: {
|
13
|
+
data: React.PropTypes.array,
|
14
|
+
isLoading: React.PropTypes.bool
|
15
|
+
},
|
16
|
+
|
17
|
+
getDefaultProps() {
|
18
|
+
return {
|
19
|
+
data: [],
|
20
|
+
isLoading: false
|
21
|
+
};
|
22
|
+
},
|
23
|
+
|
24
|
+
getInitialState() {
|
25
|
+
return {showAll: false};
|
26
|
+
},
|
27
|
+
|
28
|
+
handleClick(e) {
|
29
|
+
e.preventDefault();
|
30
|
+
|
31
|
+
this.setState({showAll: !this.state.showAll});
|
32
|
+
},
|
33
|
+
|
34
|
+
getItems() {
|
35
|
+
var items = this.props.data;
|
36
|
+
|
37
|
+
if (!this.state.showAll) {
|
38
|
+
items = take(items, 5);
|
39
|
+
}
|
40
|
+
|
41
|
+
return items.map((e) => {
|
42
|
+
return (
|
43
|
+
<li className="list-group-item list-group-item-noborder">
|
44
|
+
<RoleLink id={e} />
|
45
|
+
</li>
|
46
|
+
);
|
47
|
+
});
|
48
|
+
},
|
49
|
+
|
50
|
+
render() {
|
51
|
+
var items = this.getItems(),
|
52
|
+
msg = this.state.showAll ? 'Show top 5' : 'See all',
|
53
|
+
empty = items.length === 0,
|
54
|
+
lessThan5 = items.length <= 5,
|
55
|
+
body = [(<h2>Executors<Refresh show={this.props.isLoading} /></h2>)];
|
56
|
+
|
57
|
+
if (!empty) {
|
58
|
+
body.push(
|
59
|
+
<ul className="b-host-executors__list list-unstyled list-group">
|
60
|
+
{items}
|
61
|
+
</ul>
|
62
|
+
);
|
63
|
+
} else {
|
64
|
+
body.push(
|
65
|
+
<p>There is no executors</p>
|
66
|
+
);
|
67
|
+
}
|
68
|
+
|
69
|
+
if (!lessThan5) {
|
70
|
+
body.push(
|
71
|
+
<div className="b-host-executors__toggle">
|
72
|
+
<a href="#" onClick={this.handleClick}>{msg}</a>
|
73
|
+
</div>
|
74
|
+
);
|
75
|
+
}
|
76
|
+
|
77
|
+
return (
|
78
|
+
<div className="b-host-executors">
|
79
|
+
{body}
|
80
|
+
</div>
|
81
|
+
);
|
82
|
+
}
|
83
|
+
});
|