conjur-asset-ui-beta 1.5.0 → 1.6.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.
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
+ });