-
Notifications
You must be signed in to change notification settings - Fork 4.3k
/
Copy pathnavigate-input.js
242 lines (222 loc) · 6.91 KB
/
navigate-input.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import Ember from 'ember';
import { debounce, later } from '@ember/runloop';
import { service } from '@ember/service';
import { action } from '@ember/object';
import { guidFor } from '@ember/object/internals';
import Component from '@glimmer/component';
import { encodePath } from 'vault/utils/path-encoding-helpers';
import { keyIsFolder, parentKeyForKey } from 'core/utils/key-utils';
import keys from 'core/utils/key-codes';
/**
* @module NavigateInput
* `NavigateInput` components are used to filter list data.
*
* @example
* <NavigateInput @filter={{@roleFiltered}} urls={{hash list="vault.cluster.secrets.backend.kubernetes.roles"}}/>
*
* @param {String} filter=null - The filtered string.
* @param {String} [placeholder=Filter items] - The message inside the input to indicate what the user should enter into the space.
* @param {Object} [urls=null] - An object containing list=route url.
* @param {Function} [filterFocusDidChange=null] - A function called when the focus changes.
* @param {Function} [filterDidChange=null] - A function called when the filter string changes.
* @param {Function} [filterMatchesKey=null] - A function used to match to a specific key, such as an Id.
* @param {Function} [filterPartialMatch=null] - A function used to filter through a partial match. Such as "oo" of "root".
* @param {String} [baseKey] - A string to transition by Id.
* @param {Boolean} [shouldNavigateTree=false] - If true, navigate a larger tree, such as when you're navigating leases under access.
* @param {String} [mode=secrets] - Mode which plays into navigation type.
* @param {String} [extraNavParams] - A string used in route transition when necessary.
*/
const routeFor = function (type, mode, urls) {
const MODES = {
secrets: 'vault.cluster.secrets.backend',
'secrets-cert': 'vault.cluster.secrets.backend',
'policy-show': 'vault.cluster.policy',
'policy-list': 'vault.cluster.policies',
leases: 'vault.cluster.access.leases',
};
// urls object should have create, list, show keys
// so we'll return that here
if (urls) {
return urls[type.replace('-root', '')];
}
let useSuffix = true;
const typeVal = mode === 'secrets' || mode === 'leases' ? type : type.replace('-root', '');
const modeKey = mode + '-' + typeVal;
const modeVal = MODES[modeKey] || MODES[mode];
if (modeKey === 'policy-list') {
useSuffix = false;
}
return useSuffix ? modeVal + '.' + typeVal : modeVal;
};
export default class NavigateInput extends Component {
@service router;
inputId = `nav-input-${guidFor(this)}`;
get mode() {
return this.args.mode || 'secrets';
}
transitionToRoute(...args) {
const params = args.map((param, index) => {
if (index === 0 || typeof param !== 'string') {
return param;
}
return encodePath(param);
});
this.router.transitionTo(...params);
}
keyForNav(key) {
if (this.mode !== 'secrets-cert') {
return key;
}
return `cert/${key}`;
}
onEnter(val) {
const mode = this.mode;
const baseKey = this.args.baseKey;
const extraParams = this.args.extraNavParams;
if (mode.startsWith('secrets') && (!val || val === baseKey)) {
return;
}
if (this.args.filterMatchesKey && !keyIsFolder(val)) {
const params = [routeFor('show', mode, this.args.urls), extraParams, this.keyForNav(val)].compact();
this.transitionToRoute(...params);
} else {
if (mode === 'policies') {
return;
}
const route = routeFor('create', mode, this.args.urls);
if (baseKey) {
this.transitionToRoute(route, this.keyForNav(baseKey), {
queryParams: {
initialKey: val,
},
});
} else if (this.args.urls) {
this.transitionToRoute(route, {
queryParams: {
initialKey: this.keyForNav(val),
},
});
} else {
this.transitionToRoute(route + '-root', {
queryParams: {
initialKey: this.keyForNav(val),
},
});
}
}
}
// pop to the nearest parentKey or to the root
onEscape(val) {
const key = parentKeyForKey(val) || '';
this.args.filterDidChange(key);
this.filterUpdated(key);
}
onTab(event) {
const firstPartialMatch = this.args.firstPartialMatch?.id;
if (!firstPartialMatch) {
return;
}
event.preventDefault();
this.args.filterDidChange(firstPartialMatch);
this.filterUpdated(firstPartialMatch);
}
// as you type, navigates through the k/v tree
filterUpdated(val) {
const mode = this.mode;
if (mode === 'policies' || !this.args.shouldNavigateTree) {
this.filterUpdatedNoNav(val, mode);
return;
}
// select the key to nav to, assumed to be a folder
let key = val ? val.trim() : '';
const isFolder = keyIsFolder(key);
if (!isFolder) {
// nav to the closest parentKey (or the root)
key = parentKeyForKey(val) || '';
}
const pageFilter = val.replace(key, '');
this.navigate(this.keyForNav(key), mode, pageFilter);
}
navigate(key, mode, pageFilter) {
const route = routeFor(key ? 'list' : 'list-root', mode, this.args.urls);
const args = [route];
if (key) {
args.push(key);
}
if (pageFilter && !keyIsFolder(pageFilter)) {
args.push({
queryParams: {
page: 1,
pageFilter,
},
});
} else {
args.push({
queryParams: {
page: 1,
pageFilter: null,
},
});
}
this.transitionToRoute(...args);
}
filterUpdatedNoNav(val, mode) {
const key = val ? val.trim() : null;
this.transitionToRoute(routeFor('list-root', mode, this.args.urls), {
queryParams: {
pageFilter: key,
page: 1,
},
});
// component is not re-rendered on policy list so trigger autofocus here
this.maybeFocusInput();
}
@action
maybeFocusInput() {
// if component is loaded and filter is already applied,
// we assume the user just typed in a filter and the page reloaded
if (this.args.filter && !Ember.testing) {
later(
this,
function () {
document.getElementById(this.inputId)?.focus();
},
400
);
}
}
@action
handleInput(evt) {
if (this.args.filterDidChange) {
this.args.filterDidChange(evt.target.value);
}
debounce(this, this.filterUpdated, evt.target.value, 400);
}
@action
setFilterFocused(isFocused) {
if (this.args.filterFocusDidChange) {
this.args.filterFocusDidChange(isFocused);
}
}
@action
handleKeyPress(event) {
if (event.keyCode === keys.TAB) {
this.onTab(event);
}
}
@action
handleKeyUp(event) {
const keyCode = event.keyCode;
const val = event.target.value;
if (keyCode === keys.ENTER) {
this.onEnter(val);
}
if (keyCode === keys.ESC) {
this.onEscape(val);
}
}
}