-
Notifications
You must be signed in to change notification settings - Fork 231
/
Copy pathkeyValueEditor.js
328 lines (304 loc) · 15 KB
/
keyValueEditor.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
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
(function() {
'use strict';
angular
.module('openshiftConsole')
.directive('keyValueEditor', [
'$routeParams',
'$timeout',
'$filter',
'APIService',
'keyValueEditorConfig',
'keyValueEditorUtils',
function(
$routeParams,
$timeout,
$filter,
APIService,
config,
utils) {
var humanizeKind = $filter('humanizeKind');
var canI = $filter('canI');
var counter = 1000;
return {
restrict: 'AE',
scope: {
keyMinlength: '@', // min character length
keyMaxlength: '@', // max character length
valueMinlength: '@', // min character length
valueMaxlength: '@', // max character length
// entries: [{
// name: '' // the key... erm.
// value: '' // the value string, if no valueFrom
// cannotDelete: true || false // defaults to false
// isReadonly: true|| false // individual entries may be readonly
// isReadonlyKey: true || false // defaults to false
// isReadonlyValue: true || false // defaults to false
// keyValidator: '' // regex for validating the key (name)
// keyValidatorError: '' // text if key does not validate
// keyValidatorErrorTooltip: '' // additional text if key does not validate
// keyValidatorErrorTooltipIcon: '' // icon class to use for key tooltip
// valueValidatorError: '' // text if value does not validate
// valueIcon: '' // icon class, such as 'fa fa-lock'
// valueIconTooltip: '' // text for tooltip
// valueAlt: '' // alternative text value if valueFrom is present or complicated
// valueValidator: '' // regex for validating the value
// valueValidatorErrorTooltip: '' // additional text if value does not validate
// valueValidatorErrorTooltipIcon: '' // icon class to use for value tooltip
// INTERNAL
// apiObj: { } // INTERNAL stub of a secret or config map for url generation
// refType: '' // INTERNAL the valueFrom refType
// selectedValueFrom: {} // INTERNAL ui-select bookkeeping
// selectedValueFromKey: {} // INTERNAL custom validation error
// }]
entries: '=',
keyPlaceholder: '@',
valuePlaceholder: '@',
keyValidator: '@', // general key regex validation string
keyValidatorRegex: '=', // a regex object
valueValidator: '@', // general value regex validation string
valueValidatorRegex: '=', // a regex object
keyValidatorError: '@', // general key validation error message
keyValidatorErrorTooltip: '@',
keyValidatorErrorTooltipIcon: '@',
valueValidatorError: '@', // general value validation error message
valueValidatorErrorTooltip: '@',
valueValidatorErrorTooltipIcon: '@',
valueIconTooltip: '@', // if the tooltip for the value icon is generic
valueFromSelectorOptions: '=',
cannotAdd: '=?',
cannotSort: '=?',
cannotDelete: '=?',
isReadonly: '=?',
isReadonlyValue: '=?',
isReadonlyKeys: '=?', // will only apply to existing keys,
addRowLink: '@', // creates a link to "add row" and sets its text label
addRowWithSelectorsLink: '@', // creates a link to "add row with selectors" and sets its text label
showHeader: '=?', // show placeholder text as headers
allowEmptyKeys: '=?',
keyRequiredError: '@'
},
templateUrl: 'views/directives/key-value-editor.html',
link: function($scope, $elem, $attrs) {
var unwatchEntries;
// validation is irritating.
$scope.validation = {
key: $scope.keyValidator,
val: $scope.valueValidator
};
// override if we get a regex literal
if($attrs.keyValidatorRegex) {
$scope.validation.key = $scope.keyValidatorRegex;
}
if($attrs.valueValidatorRegex) {
$scope.validation.val = $scope.valueValidatorRegex;
}
if('grabFocus' in $attrs) {
$scope.grabFocus = true;
// after render set to undefined to ensure it doesn't keep trying to grab focus
$timeout(function() {
$scope.grabFocus = undefined;
});
}
// if an attribute exists, set its corresponding bool to true
if('cannotAdd' in $attrs) {
$scope.cannotAdd = true;
}
if('cannotDelete' in $attrs) {
$scope.cannotDeleteAny = true;
}
if('isReadonly' in $attrs) {
$scope.isReadonlyAny = true;
}
// only applies to the initial set, if a user adds an entry the
// user must be allowed to set the key!
if('isReadonlyKeys' in $attrs) {
// the $scope.$watch here lets us wait until we are certain we get
// a legitimate first set, perhaps after a promise resolution, run the
// update, then unregister.
unwatchEntries = $scope.$watch('entries', function(newVal) {
if(newVal) {
_.each($scope.entries, function(entry) {
entry.isReadonlyKey = true;
});
unwatchEntries();
}
});
}
if('cannotSort' in $attrs) {
$scope.cannotSort = true;
}
if('showHeader' in $attrs) {
$scope.showHeader = true;
}
if('allowEmptyKeys' in $attrs) {
$scope.allowEmptyKeys = true;
}
$scope.groupByKind = function(object) {
return humanizeKind(object.kind);
};
$scope.valueFromObjectSelected = function(entry, selected) {
if (selected.kind === 'ConfigMap') {
entry.valueFrom.configMapKeyRef = {
name: selected.metadata.name
};
delete entry.valueFrom.secretKeyRef;
} else if (selected.kind === 'Secret') {
entry.valueFrom.secretKeyRef = {
name: selected.metadata.name
};
delete entry.valueFrom.configMapKeyRef;
}
delete entry.selectedValueFromKey;
};
$scope.valueFromKeySelected = function(entry, selected) {
if (entry.valueFrom.configMapKeyRef) {
entry.valueFrom.configMapKeyRef.key = selected;
return;
} else if (entry.valueFrom.secretKeyRef) {
entry.valueFrom.secretKeyRef.key = selected;
return;
}
};
// min/max lengths
angular.extend($scope, {
keyMinlength: config.keyMinlength || $attrs.keyMinlength,
keyMaxlength: config.keyMaxlength || $attrs.keyMaxlength,
valueMinlength: config.valueMinlength || $attrs.valueMinlength,
valueMaxlength: config.valueMaxlength || $attrs.valueMaxlength,
// validation regex
keyValidator: config.keyValidator || $attrs.keyValidator,
valueValidator: config.valueValidator || $attrs.valueValidator,
keyValidatorError: config.keyValidatorError || $attrs.keyValidatorError,
valueValidatorError: config.valueValidatorError || $attrs.valueValidatorError,
keyRequiredError: config.keyRequiredError || $attrs.keyRequiredError,
// validation error tooltip
keyValidatorErrorTooltip: config.keyValidatorErrorTooltip || $attrs.keyValidatorErrorTooltip,
keyValidatorErrorTooltipIcon: config.keyValidatorErrorTooltipIcon || $attrs.keyValidatorErrorTooltipIcon,
valueValidatorErrorTooltip: config.valueValidatorErrorTooltip || $attrs.valueValidatorErrorTooltip,
valueValidatorErrorTooltipIcon: config.valueValidatorErrorTooltipIcon || $attrs.valueValidatorErrorTooltipIcon,
// placeholders
keyPlaceholder: config.keyPlaceholder || $attrs.keyPlaceholder,
valuePlaceholder: config.valuePlaceholder || $attrs.valuePlaceholder
});
},
controller: [
'$scope',
function($scope) {
var readOnlySome = [];
var cannotDeleteSome = [];
var unique = counter++;
$scope.configMapVersion = APIService.getPreferredVersion('configmaps');
$scope.secretsVersion = APIService.getPreferredVersion('secrets');
var canIGetSecrets = canI($scope.secretsVersion, 'get');
var canIGetConfigMaps = canI($scope.configMapVersion, 'get');
angular.extend($scope, {
namespace: $routeParams.project,
unique: unique,
forms: {},
placeholder: utils.newEntry(),
setFocusKeyClass: 'key-value-editor-set-focus-key-' + unique,
setFocusValClass: 'key-value-editor-set-focus-value-' + unique,
uniqueForKey: utils.uniqueForKey,
uniqueForValue: utils.uniqueForValue,
dragControlListeners: {
// only allow sorting within the parent instance
accept: function (sourceItemHandleScope, destSortableScope) {
return sourceItemHandleScope.itemScope.sortableScope.$id === destSortableScope.$id;
},
orderChanged: function() {
$scope.forms.keyValueEditor.$setDirty();
}
},
deleteEntry: function(start, deleteCount) {
$scope.entries.splice(start, deleteCount);
// if the link is used, add a new empty entry to ensure the inputs do not all disappear
if(!$scope.entries.length && $scope.addRowLink) {
utils.addEntry($scope.entries);
}
$scope.forms.keyValueEditor.$setDirty();
},
isReadonlySome: function(name) {
return _.includes(readOnlySome, name);
},
cannotDeleteSome: function(name) {
return _.includes(cannotDeleteSome, name);
},
onAddRow: function() {
utils.addEntry($scope.entries);
utils.setFocusOn('.'+ $scope.setFocusKeyClass);
},
onAddRowWithSelectors: function() {
utils.addEntryWithSelectors($scope.entries);
utils.setFocusOn('.'+ $scope.setFocusKeyClass);
},
isValueFromReadonly: function(entry) {
return $scope.isReadonlyAny ||
entry.isReadonlyValue ||
// set to a valueFrom && can find the object in valueFromSelectorOptions
(entry.refType && !entry.selectedValueFrom) ||
_.isEmpty($scope.valueFromSelectorOptions);
}
});
// Issue #78 todo:
// https://github.com/openshift/angular-key-value-editor/issues/78
// cannotDelete and isReadonly are boolean or list values.
// if boolean, they apply to all.
// if arrays, they apply to the items passed.
// GOTCHA:
// we suppport:
// <key-value-editor is-readonly cannot-delete>
// and:
// <key-value-editor is-readonly="['foo']" cannot-delete="['foo','bar']">
// changing the is-readonly and cannot-delete to a list and then
// setting the list to undefined/null will not:
// cannotDeleteAny = false;
// why?
// we assume the presence of is-readonly similar to disabled and other html
// attributes that are 'truthy' though they have no actual value.
// workaround?
// potentially using ng-attr-cannot-delete=false?
$scope.$watch('cannotDelete', function(newVal) {
if(angular.isArray(newVal)) {
$scope.cannotDeleteAny = false;
cannotDeleteSome = newVal;
}
});
$scope.$watch('isReadonly', function(newVal) {
if(angular.isArray(newVal)) {
$scope.isReadonlyAny = false;
readOnlySome = newVal;
}
});
// watching the attribute allows both:
// <key-value-editor add-row-link>
// <key-value-editor add-row-link="Add a pair">
$scope.$watch('addRowLink', function(newVal) {
$scope.addRowLink = newVal || 'Add row';
if($scope.entries && !$scope.entries.length) {
utils.addEntry($scope.entries);
}
});
// ensures we always have at least one set of inputs
$scope.$watch('entries', function(newVal) {
// entries MUST be an array. if we get an empty array,
// we add an empty entry to ensure the inputs show.
// NOTE: entries must be an array, with a .push() method
// else addEntry() will fail.
if(newVal && !newVal.length) {
utils.addEntry($scope.entries);
}
// check valueFrom attribs and set an alt text for display if present
_.each($scope.entries, function(entry) {
utils.altTextForValueFrom(entry, $scope.namespace);
utils.setEntryPerms(entry, canIGetSecrets, canIGetConfigMaps);
});
utils.findReferenceValueForEntries(newVal, $scope.valueFromSelectorOptions);
});
$scope.$watch('valueFromSelectorOptions', function() {
utils.findReferenceValueForEntries($scope.entries, $scope.valueFromSelectorOptions);
});
}
]
};
}]);
})();