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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/TODO.md +0 -23
  3. data/app/build/js/app.js +72652 -50337
  4. data/app/package.json +2 -0
  5. data/app/src/actions.js +56 -3
  6. data/app/src/app.js +7 -3
  7. data/app/src/clients/graph.js +24 -0
  8. data/app/src/clients/members.js +3 -3
  9. data/app/src/components/audit/table.js +9 -0
  10. data/app/src/components/custom/view.js +26 -14
  11. data/app/src/components/dashboard/activity.js +24 -4
  12. data/app/src/components/generic/foldable_audit_section.js +17 -0
  13. data/app/src/components/generic/role_link.js +2 -1
  14. data/app/src/components/graph/graph.js +421 -0
  15. data/app/src/components/graph/graph.less +39 -0
  16. data/app/src/components/group/view.js +31 -14
  17. data/app/src/components/host/activity.js +24 -4
  18. data/app/src/components/host/executors.js +83 -0
  19. data/app/src/components/host/updaters.js +83 -0
  20. data/app/src/components/host/view.js +46 -14
  21. data/app/src/components/layer/view.js +30 -13
  22. data/app/src/components/policy/view.js +23 -14
  23. data/app/src/components/search/search.js +21 -7
  24. data/app/src/components/user/activity.js +25 -4
  25. data/app/src/components/user/view.js +30 -13
  26. data/app/src/components/variable/activity.js +25 -4
  27. data/app/src/components/variable/fetchers.js +1 -1
  28. data/app/src/components/variable/updaters.js +1 -1
  29. data/app/src/components/variable/view.js +24 -13
  30. data/app/src/constants.js +4 -2
  31. data/app/src/stores/graph_store.js +55 -0
  32. data/app/src/stores/host_store.js +12 -1
  33. data/app/src/stores/route_store.js +7 -5
  34. data/app/src/stores/search_store.js +11 -6
  35. data/conjur-asset-ui.gemspec +1 -1
  36. data/lib/conjur-asset-ui-version.rb +1 -1
  37. 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'), Router.State, TabMixin],
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 = unescape(this.getParams().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
- <div className="b-host-activity__graph">
87
- <Chart options={this.props.options}
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
+ });