admission 0.2.1 → 0.2.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +1 -5
- data/bin/build +10 -0
- data/lib/admission/arbitration.rb +1 -0
- data/lib/admission/resource_arbitration.rb +4 -0
- data/lib/admission/version.rb +1 -1
- data/lib/admission/visualisation_app.rb +125 -0
- data/visualisation/components/app_container.jsx +7 -2
- data/visualisation/components/input_with_select.jsx +30 -17
- data/visualisation/components/nested_list_row.jsx +48 -0
- data/visualisation/components/privilege_select.jsx +28 -9
- data/visualisation/components/privileges_panel.jsx +62 -3
- data/visualisation/components/rules_panel.jsx +124 -0
- data/visualisation/dist/app.js +6 -0
- data/visualisation/helpers.js +69 -0
- data/visualisation/index.jsx +17 -60
- data/visualisation/package.json +4 -1
- data/visualisation/reducers/index.js +9 -4
- data/visualisation/server.rb +23 -0
- data/visualisation/style.scss +124 -19
- data/visualisation/webpack.config.js +13 -3
- data/visualisation/yarn.lock +28 -0
- metadata +9 -4
- data/bin/server.rb +0 -26
- data/lib/admission/visualisation.rb +0 -81
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 62f774b23937957f5d4fd0dcabda6c136e393f71
|
4
|
+
data.tar.gz: 6d2efadac8eaccc3606a75d85807908ec00b88d1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c3f1a68687bf1f029c272e9451dadb3aad77b50b5fa35228595541fb3eb861f1da1f803b4eeb79f8fd84e33b73b530c1ec4d2393223b6986e430a29eca6b10a5
|
7
|
+
data.tar.gz: 4851ad4ad9dcb1f6e61538829749127c79ae16a413532e4da99e7dc599f86103b1001fc94618f9f276277edf779b8959520b6b2bf2b03c2a7e8cf642a06560e1
|
data/Gemfile
CHANGED
data/bin/build
ADDED
data/lib/admission/version.rb
CHANGED
@@ -0,0 +1,125 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
class Admission::VisualisationApp
|
5
|
+
|
6
|
+
def initialize **settings
|
7
|
+
order = settings[:order] || (raise 'order not defined, cannot visualise data')
|
8
|
+
raise 'order must be a Proc' unless Proc === order
|
9
|
+
|
10
|
+
settings[:js_entry] ||= Pathname.new(__FILE__).join('..', '..', '..',
|
11
|
+
'visualisation', 'dist', 'app.js')
|
12
|
+
|
13
|
+
@settings = settings
|
14
|
+
end
|
15
|
+
|
16
|
+
def call env
|
17
|
+
case env['PATH_INFO'].to_s
|
18
|
+
when %r~^/(\.html)?$~
|
19
|
+
[
|
20
|
+
200,
|
21
|
+
{'Content-Type' => 'text/html'},
|
22
|
+
[render_page]
|
23
|
+
]
|
24
|
+
|
25
|
+
when %r~/app\.js~
|
26
|
+
[
|
27
|
+
200,
|
28
|
+
{'Content-Type' => 'application/js'},
|
29
|
+
[File.read(@settings[:js_entry])]
|
30
|
+
]
|
31
|
+
|
32
|
+
when %r~/data\.json~
|
33
|
+
[
|
34
|
+
200,
|
35
|
+
{'Content-Type' => 'application/json'},
|
36
|
+
[render_data(@settings[:order].call)]
|
37
|
+
]
|
38
|
+
|
39
|
+
else
|
40
|
+
[
|
41
|
+
404,
|
42
|
+
{'Content-Type' => 'text/html'},
|
43
|
+
['Admission::VisualisationApp : page not found']
|
44
|
+
]
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def render_page
|
50
|
+
data_url = "#{@settings[:url_prefix]}/data.json"
|
51
|
+
script_url = "#{@settings[:url_prefix]}/app.js"
|
52
|
+
|
53
|
+
<<-HEREDOC
|
54
|
+
<!DOCTYPE html>
|
55
|
+
<html>
|
56
|
+
<head>
|
57
|
+
<title>Admission</title>
|
58
|
+
</head>
|
59
|
+
<body class='flex-column'>
|
60
|
+
|
61
|
+
<div data-url="#{data_url}" id="admission-visualisation"></div>
|
62
|
+
<script src="#{script_url}"></script>
|
63
|
+
|
64
|
+
</body>
|
65
|
+
</html>
|
66
|
+
HEREDOC
|
67
|
+
end
|
68
|
+
|
69
|
+
def render_data privileges:, rules:, arbitrator: Admission::ResourceArbitration
|
70
|
+
js_data = {}
|
71
|
+
|
72
|
+
top_levels = []
|
73
|
+
privileges = privileges.values.inject Array.new do |arr, levels|
|
74
|
+
tops, others = levels.to_a.partition{|key, _| key == :'^'}
|
75
|
+
tops.first[1].tap{|privilege| top_levels << privilege.text_key}
|
76
|
+
|
77
|
+
others.each do |_, privilege|
|
78
|
+
arr << {name: privilege.name, level: privilege.level,
|
79
|
+
inherits: privilege.inherited && privilege.inherited.map(&:text_key)}
|
80
|
+
end
|
81
|
+
|
82
|
+
arr
|
83
|
+
end
|
84
|
+
|
85
|
+
js_data[:privileges] = privileges
|
86
|
+
js_data[:top_levels] = top_levels
|
87
|
+
js_data[:levels] = privileges.inject Hash.new do |hash, p|
|
88
|
+
(hash[p[:name]] ||= []) << p[:level]
|
89
|
+
hash
|
90
|
+
end
|
91
|
+
|
92
|
+
actions_reduce = -> (index) {
|
93
|
+
index.to_a.map do |action, action_index|
|
94
|
+
action_index = action_index.to_a.map do |privilege, rule|
|
95
|
+
if rule.is_a? Proc
|
96
|
+
rule = 'proc'
|
97
|
+
end
|
98
|
+
|
99
|
+
[privilege.text_key, rule]
|
100
|
+
end
|
101
|
+
|
102
|
+
[action, Hash[action_index]]
|
103
|
+
end
|
104
|
+
}
|
105
|
+
|
106
|
+
rules = if arbitrator == Admission::Arbitration
|
107
|
+
single_scope = actions_reduce[rules]
|
108
|
+
[['-non-scoped-', Hash[single_scope]]]
|
109
|
+
|
110
|
+
elsif arbitrator == Admission::ResourceArbitration
|
111
|
+
rules.to_a.map do |scope, scope_index|
|
112
|
+
scope_index = actions_reduce[scope_index]
|
113
|
+
[scope, Hash[scope_index]]
|
114
|
+
end
|
115
|
+
|
116
|
+
else
|
117
|
+
raise "not implemented for #{arbitrator.name}"
|
118
|
+
|
119
|
+
end
|
120
|
+
js_data[:rules] = Hash[rules]
|
121
|
+
|
122
|
+
JSON.generate js_data
|
123
|
+
end
|
124
|
+
|
125
|
+
end
|
@@ -1,7 +1,9 @@
|
|
1
1
|
import preact from 'preact';
|
2
|
-
import PrivilegesPanel from './privileges_panel';
|
3
2
|
import classnames from 'classnames';
|
4
3
|
|
4
|
+
import PrivilegesPanel from './privileges_panel';
|
5
|
+
import RulesPanel from "./rules_panel";
|
6
|
+
|
5
7
|
export default class AppContainer extends preact.Component {
|
6
8
|
|
7
9
|
constructor (props) {
|
@@ -21,7 +23,7 @@ export default class AppContainer extends preact.Component {
|
|
21
23
|
<code>{load_fail}</code>
|
22
24
|
</div>;
|
23
25
|
|
24
|
-
return <div>
|
26
|
+
return <div className="admission-app-container">
|
25
27
|
<ul className="panels-list">
|
26
28
|
|
27
29
|
<li
|
@@ -68,6 +70,9 @@ export default class AppContainer extends preact.Component {
|
|
68
70
|
return <PrivilegesPanel app={app} />;
|
69
71
|
break;
|
70
72
|
|
73
|
+
case 'rules':
|
74
|
+
return <RulesPanel app={app}/>;
|
75
|
+
break;
|
71
76
|
}
|
72
77
|
}
|
73
78
|
}
|
@@ -1,5 +1,6 @@
|
|
1
1
|
import preact from 'preact';
|
2
2
|
import classnames from 'classnames';
|
3
|
+
import helpers from '../helpers';
|
3
4
|
|
4
5
|
export default class InputWithSelect extends preact.Component {
|
5
6
|
|
@@ -13,18 +14,19 @@ export default class InputWithSelect extends preact.Component {
|
|
13
14
|
|
14
15
|
this.setParentRef = ref => this.element = ref;
|
15
16
|
this.setListRef = ref => this.list = ref;
|
16
|
-
this.onKeyDown =
|
17
|
-
this.
|
17
|
+
this.onKeyDown = this.onKeyDown.bind(this);
|
18
|
+
this.onTextChange = helpers.debounce(this.onTextChange.bind(this), 400);
|
19
|
+
this.toggleList = helpers.debounce(this.toggleList.bind(this), 400, true);
|
18
20
|
this.closeList = this.closeList.bind(this);
|
19
21
|
this.onSelected = this.onSelected.bind(this);
|
20
22
|
}
|
21
23
|
|
22
|
-
render ({
|
24
|
+
render ({placeholder}, {text, matching}) {
|
23
25
|
if (!matching) this.list = null;
|
24
26
|
|
25
27
|
return <div
|
26
28
|
ref={this.setParentRef}
|
27
|
-
className="
|
29
|
+
className="select_box">
|
28
30
|
|
29
31
|
<div className="_inputs">
|
30
32
|
<input
|
@@ -35,6 +37,7 @@ export default class InputWithSelect extends preact.Component {
|
|
35
37
|
value={text}/>
|
36
38
|
|
37
39
|
<button
|
40
|
+
type="button"
|
38
41
|
tabIndex="-1"
|
39
42
|
className="button"
|
40
43
|
onClick={this.toggleList}>
|
@@ -44,7 +47,6 @@ export default class InputWithSelect extends preact.Component {
|
|
44
47
|
|
45
48
|
{matching && <DropdownList
|
46
49
|
ref={this.setListRef}
|
47
|
-
app={app}
|
48
50
|
items={matching}
|
49
51
|
toSelect={this.onSelected}
|
50
52
|
toClose={this.closeList}
|
@@ -72,11 +74,18 @@ export default class InputWithSelect extends preact.Component {
|
|
72
74
|
|
73
75
|
onKeyDown (e) {
|
74
76
|
if (this.list && this.list.onKeyDown(e)) return;
|
77
|
+
if (e.keyCode === 13 && this.props.enterable) {
|
78
|
+
this.onSelected(e.target.value.trim());
|
79
|
+
return;
|
80
|
+
}
|
81
|
+
this.onTextChange(e);
|
82
|
+
}
|
75
83
|
|
84
|
+
onTextChange (e) {
|
76
85
|
const text = e.target.value.trim();
|
77
86
|
let matching = null;
|
78
87
|
if ((text && text !==this.state.text) || e.keyCode === 40) {
|
79
|
-
matching =
|
88
|
+
matching = this.filtered_items(text);
|
80
89
|
}
|
81
90
|
this.setState({matching, text});
|
82
91
|
}
|
@@ -86,7 +95,7 @@ export default class InputWithSelect extends preact.Component {
|
|
86
95
|
this.closeList();
|
87
96
|
|
88
97
|
} else {
|
89
|
-
this.setState({matching: this.
|
98
|
+
this.setState({matching: this.filtered_items()});
|
90
99
|
}
|
91
100
|
}
|
92
101
|
|
@@ -99,6 +108,14 @@ export default class InputWithSelect extends preact.Component {
|
|
99
108
|
this.props.onSelect(value);
|
100
109
|
}
|
101
110
|
|
111
|
+
filtered_items (text) {
|
112
|
+
let items = this.props.all_items;
|
113
|
+
if (text) items = items.filter(value => value.startsWith(text));
|
114
|
+
if (this.props.nullable) items.unshift(null);
|
115
|
+
|
116
|
+
return items.length === 0 ? null : items;
|
117
|
+
}
|
118
|
+
|
102
119
|
}
|
103
120
|
|
104
121
|
class DropdownList extends preact.Component {
|
@@ -108,14 +125,14 @@ class DropdownList extends preact.Component {
|
|
108
125
|
this.state = {selected: -1};
|
109
126
|
}
|
110
127
|
|
111
|
-
render ({
|
128
|
+
render ({items, toSelect}, {selected}) {
|
112
129
|
return <div
|
113
130
|
className="_dropdown">
|
114
131
|
<ul>
|
115
132
|
{items.map((name, i) => <li
|
116
133
|
className={classnames({'selected': selected === i})}
|
117
134
|
onClick={() => toSelect(name)}>
|
118
|
-
{name}
|
135
|
+
{name === null ? '\u00A0' : name}
|
119
136
|
</li>)}
|
120
137
|
</ul>
|
121
138
|
</div>;
|
@@ -135,8 +152,10 @@ class DropdownList extends preact.Component {
|
|
135
152
|
|
136
153
|
case 13: // enter
|
137
154
|
const selected = this.state.selected;
|
138
|
-
if (selected !== -1)
|
139
|
-
|
155
|
+
if (selected !== -1) {
|
156
|
+
this.props.toSelect(this.props.items[selected]);
|
157
|
+
return true;
|
158
|
+
}
|
140
159
|
break;
|
141
160
|
|
142
161
|
case 27: // escape
|
@@ -155,10 +174,4 @@ class DropdownList extends preact.Component {
|
|
155
174
|
if (selected >= this.props.items.length) selected = this.props.items.length -1;
|
156
175
|
this.setState({selected});
|
157
176
|
}
|
158
|
-
}
|
159
|
-
|
160
|
-
function filter_items (all, input_text) {
|
161
|
-
let items = all.filter(value => value.startsWith(input_text));
|
162
|
-
if (items.length === 0) items = null;
|
163
|
-
return items;
|
164
177
|
}
|
@@ -0,0 +1,48 @@
|
|
1
|
+
import preact from 'preact';
|
2
|
+
|
3
|
+
export default class NestedListRow extends preact.Component {
|
4
|
+
|
5
|
+
constructor (props) {
|
6
|
+
super(props);
|
7
|
+
|
8
|
+
this.state = {
|
9
|
+
unrolled: !!props.defaultUnrolled
|
10
|
+
};
|
11
|
+
|
12
|
+
this.toggleRollOut = this.toggleRollOut.bind(this);
|
13
|
+
}
|
14
|
+
|
15
|
+
render ({app, content, nestedRows}, {unrolled}) {
|
16
|
+
return <li>
|
17
|
+
<div onClick={this.toggleRollOut} className="nested-list-content">
|
18
|
+
{nestedRows && <span className="icon">
|
19
|
+
{unrolled ? '\u25B6' : '\u25BC'}
|
20
|
+
</span>
|
21
|
+
}
|
22
|
+
<span className="content">{content}</span>
|
23
|
+
</div>
|
24
|
+
{unrolled && nestedRows && <ul className="nested-list">{
|
25
|
+
nestedRows.map(row =>
|
26
|
+
<NestedListRow
|
27
|
+
app={app}
|
28
|
+
content={row.content}
|
29
|
+
nestedRows={row.nested_rows}
|
30
|
+
defaultUnrolled={this.props.defaultUnrolled}
|
31
|
+
/>
|
32
|
+
)
|
33
|
+
}</ul>}
|
34
|
+
</li>;
|
35
|
+
}
|
36
|
+
|
37
|
+
componentWillReceiveProps ({defaultUnrolled}) {
|
38
|
+
if (defaultUnrolled !== this.props.defaultUnrolled) {
|
39
|
+
this.setState({unrolled: !!defaultUnrolled});
|
40
|
+
}
|
41
|
+
}
|
42
|
+
|
43
|
+
toggleRollOut () {
|
44
|
+
this.setState({unrolled: !this.state.unrolled});
|
45
|
+
}
|
46
|
+
|
47
|
+
}
|
48
|
+
|
@@ -6,19 +6,16 @@ export default class PrivilegeSelect extends preact.Component {
|
|
6
6
|
constructor (props) {
|
7
7
|
super(props);
|
8
8
|
|
9
|
-
this.state =
|
10
|
-
name: '',
|
11
|
-
level: ''
|
12
|
-
};
|
9
|
+
this.state = props.app.keyToPrivilege(props.defaultValue);
|
13
10
|
|
14
11
|
this.onNameSelected = this.onNameSelected.bind(this);
|
15
12
|
this.onLevelSelected = this.onLevelSelected.bind(this);
|
13
|
+
this.onClearSelection = this.onClearSelection.bind(this);
|
16
14
|
}
|
17
15
|
|
18
16
|
render ({app}, {name, level}) {
|
19
|
-
return <div className="controls-
|
17
|
+
return <div className="controls-group">
|
20
18
|
<InputWithSelect
|
21
|
-
app={app}
|
22
19
|
defaultText={name}
|
23
20
|
placeholder="name"
|
24
21
|
all_items={app.listPrivilegesNames()}
|
@@ -26,18 +23,34 @@ export default class PrivilegeSelect extends preact.Component {
|
|
26
23
|
/>
|
27
24
|
|
28
25
|
<InputWithSelect
|
29
|
-
app={app}
|
30
26
|
defaultText={level}
|
31
27
|
placeholder="level"
|
32
28
|
all_items={app.listPrivilegeLevels(name)}
|
33
29
|
onSelect={this.onLevelSelected}
|
34
30
|
/>
|
31
|
+
|
32
|
+
<button
|
33
|
+
type="button"
|
34
|
+
tabIndex="-1"
|
35
|
+
className="button"
|
36
|
+
onClick={this.onClearSelection}>
|
37
|
+
Clear
|
38
|
+
</button>
|
35
39
|
</div>;
|
36
40
|
}
|
37
41
|
|
42
|
+
componentWillReceiveProps ({defaultValue}) {
|
43
|
+
if (defaultValue !== this.props.defaultValue) {
|
44
|
+
this.setState(this.props.app.keyToPrivilege(defaultValue));
|
45
|
+
}
|
46
|
+
}
|
47
|
+
|
38
48
|
onNameSelected (name) {
|
39
|
-
|
40
|
-
|
49
|
+
const levels = this.props.app.admission.levels[name];
|
50
|
+
let level;
|
51
|
+
if (!levels) level = 'base';
|
52
|
+
else level = levels[levels.length - 1];
|
53
|
+
|
41
54
|
this.setState({name, level});
|
42
55
|
this.props.onChanged({name, level});
|
43
56
|
}
|
@@ -48,4 +61,10 @@ export default class PrivilegeSelect extends preact.Component {
|
|
48
61
|
this.props.onChanged({name, level});
|
49
62
|
}
|
50
63
|
|
64
|
+
onClearSelection () {
|
65
|
+
const selection = {name: '', level: ''};
|
66
|
+
this.setState(selection);
|
67
|
+
this.props.onChanged(selection);
|
68
|
+
}
|
69
|
+
|
51
70
|
}
|