@rancher/shell 0.3.23 → 0.3.24

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 (57) hide show
  1. package/assets/styles/base/_variables.scss +1 -0
  2. package/assets/styles/themes/_dark.scss +1 -0
  3. package/assets/styles/themes/_light.scss +6 -5
  4. package/assets/translations/en-us.yaml +15 -10
  5. package/assets/translations/zh-hans.yaml +1 -1
  6. package/components/ClusterProviderIconMenu.vue +161 -0
  7. package/components/Loading.vue +1 -1
  8. package/components/SideNav.vue +1 -1
  9. package/components/form/SelectOrCreateAuthSecret.vue +7 -0
  10. package/components/nav/Group.vue +54 -24
  11. package/components/nav/Header.vue +1 -1
  12. package/components/nav/TopLevelMenu.vue +469 -294
  13. package/components/nav/Type.vue +31 -5
  14. package/creators/pkg/init +2 -2
  15. package/edit/fleet.cattle.io.gitrepo.vue +43 -15
  16. package/edit/logging.banzaicloud.io.output/index.vue +7 -0
  17. package/edit/provisioning.cattle.io.cluster/CustomCommand.vue +3 -8
  18. package/edit/provisioning.cattle.io.cluster/rke2.vue +108 -33
  19. package/edit/workload/storage/ContainerMountPaths.vue +7 -5
  20. package/initialize/App.js +2 -0
  21. package/initialize/client.js +63 -51
  22. package/initialize/index.js +2 -0
  23. package/layouts/default.vue +8 -0
  24. package/machine-config/amazonec2.vue +1 -0
  25. package/mixins/fetch.client.js +3 -3
  26. package/package.json +1 -1
  27. package/pages/__tests__/prefs.test.ts +1 -1
  28. package/pages/c/_cluster/explorer/ConfigBadge.vue +1 -0
  29. package/pages/prefs.vue +3 -13
  30. package/plugins/dashboard-store/resource-class.js +1 -1
  31. package/public/index.html +4 -2
  32. package/rancher-components/BadgeState/BadgeState.vue +5 -1
  33. package/rancher-components/Banner/Banner.test.ts +51 -1
  34. package/rancher-components/Banner/Banner.vue +134 -53
  35. package/rancher-components/Card/Card.test.ts +37 -0
  36. package/rancher-components/Card/Card.vue +24 -7
  37. package/rancher-components/Form/Checkbox/Checkbox.test.ts +20 -29
  38. package/rancher-components/Form/Checkbox/Checkbox.vue +45 -20
  39. package/rancher-components/Form/LabeledInput/LabeledInput.test.ts +2 -8
  40. package/rancher-components/Form/LabeledInput/LabeledInput.vue +30 -10
  41. package/rancher-components/Form/Radio/RadioButton.test.ts +35 -0
  42. package/rancher-components/Form/Radio/RadioButton.vue +30 -13
  43. package/rancher-components/Form/Radio/RadioGroup.vue +26 -7
  44. package/rancher-components/Form/TextArea/TextAreaAutoGrow.vue +7 -6
  45. package/rancher-components/Form/ToggleSwitch/ToggleSwitch.test.ts +25 -38
  46. package/rancher-components/Form/ToggleSwitch/ToggleSwitch.vue +23 -11
  47. package/rancher-components/LabeledTooltip/LabeledTooltip.vue +19 -5
  48. package/rancher-components/StringList/StringList.test.ts +453 -49
  49. package/rancher-components/StringList/StringList.vue +92 -58
  50. package/scripts/extension/parse-tag-name +0 -0
  51. package/store/prefs.js +3 -4
  52. package/store/type-map.js +2 -16
  53. package/types/shell/index.d.ts +13 -10
  54. package/utils/__tests__/sort.test.ts +61 -0
  55. package/utils/string.js +12 -0
  56. package/vue.config.js +1 -4
  57. package/yarn-error.log +200 -0
@@ -30,9 +30,9 @@ const CLASS = {
30
30
  * Manage a list of strings
31
31
  */
32
32
  export default Vue.extend({
33
- components: { LabeledInput },
34
33
 
35
- name: 'StringList',
34
+ name: 'StringList',
35
+ components: { LabeledInput },
36
36
 
37
37
  props: {
38
38
  /**
@@ -85,11 +85,11 @@ export default Vue.extend({
85
85
  },
86
86
  data() {
87
87
  return {
88
- value: null as string | null,
89
- selected: null as string | null,
90
- isEditItem: null as string | null,
91
- isCreateItem: false,
92
- errors: { duplicate: false } as Record<Error, boolean>
88
+ value: null as string | null,
89
+ selected: null as string | null,
90
+ editedItem: null as string | null,
91
+ isCreateItem: false,
92
+ errors: { duplicate: false } as Record<Error, boolean>
93
93
  };
94
94
  },
95
95
 
@@ -100,8 +100,8 @@ export default Vue.extend({
100
100
  */
101
101
  errorMessagesArray(): string[] {
102
102
  return (Object.keys(this.errors) as Error[])
103
- .filter(f => !!(this.errors)[f])
104
- .map(k => this.errorMessages[k]);
103
+ .filter((f) => this.errors[f] && this.errorMessages[f])
104
+ .map((k) => this.errorMessages[k]);
105
105
  },
106
106
  },
107
107
 
@@ -113,23 +113,35 @@ export default Vue.extend({
113
113
  this.toggleEditMode(false);
114
114
  this.toggleCreateMode(false);
115
115
  },
116
+ value(val) {
117
+ this.$emit('type:item', val);
118
+ },
119
+ errors: {
120
+ handler(val) {
121
+ this.$emit('errors', val);
122
+ },
123
+ deep: true
124
+ }
116
125
  },
117
126
 
118
127
  methods: {
119
128
  onChange(value: string) {
120
129
  this.value = value;
121
- /**
122
- * Remove duplicate error when a new value is typed
123
- */
130
+
131
+ const items = [
132
+ ...this.items,
133
+ this.value
134
+ ];
135
+
124
136
  this.toggleError(
125
137
  'duplicate',
126
- false,
127
- this.isCreateItem ? INPUT.create : INPUT.edit,
138
+ hasDuplicatedStrings(items, this.caseSensitive),
139
+ this.isCreateItem ? INPUT.create : INPUT.edit
128
140
  );
129
141
  },
130
142
 
131
143
  onSelect(item: string) {
132
- if (this.isCreateItem || this.isEditItem === item) {
144
+ if (this.readonly || this.isCreateItem || this.editedItem === item) {
133
145
  return;
134
146
  }
135
147
  this.selected = item;
@@ -160,7 +172,7 @@ export default Vue.extend({
160
172
  },
161
173
 
162
174
  onClickEmptyBody() {
163
- if (!this.isCreateItem && !this.isEditItem) {
175
+ if (!this.isCreateItem && !this.editedItem) {
164
176
  this.toggleCreateMode(true);
165
177
  }
166
178
  },
@@ -176,38 +188,43 @@ export default Vue.extend({
176
188
 
177
189
  return;
178
190
  }
179
- if (this.isEditItem) {
191
+ if (this.editedItem) {
192
+ this.deleteAndSelectNext(this.editedItem);
180
193
  this.toggleEditMode(false);
181
194
 
182
195
  return;
183
196
  }
184
197
  if (this.selected) {
185
- const index = findStringIndex(this.items, this.selected, false);
198
+ this.deleteAndSelectNext(this.selected);
199
+ }
200
+ },
186
201
 
187
- if (index !== -1) {
188
- /**
189
- * Select the next item in the list when an item is to be deleted.
190
- */
191
- const item = (this.items[index + 1] || this.items[index - 1]);
202
+ deleteAndSelectNext(currItem: string) {
203
+ const index = findStringIndex(this.items, currItem, false);
204
+
205
+ if (index !== -1) {
206
+ /**
207
+ * Select the next item in the list.
208
+ */
209
+ const item = (this.items[index + 1] || this.items[index - 1]);
192
210
 
193
- this.onSelect(item);
194
- this.setFocus(item);
211
+ this.onSelect(item);
212
+ this.setFocus(item);
195
213
 
196
- this.deleteItem(this.items[index]);
197
- }
214
+ this.deleteItem(this.items[index]);
198
215
  }
199
216
  },
200
217
 
201
218
  setFocus(refId: string) {
202
- this.$nextTick(() => this.getElemByRef(refId)?.focus());
219
+ this.$nextTick(() => (this.getElemByRef(refId) as Vue & HTMLElement)?.focus());
203
220
  },
204
221
 
205
222
  /**
206
223
  * Move scrollbar when the selected item is over the top or bottom side of the box
207
224
  */
208
225
  moveScrollbar(arrow: Arrow, value?: number) {
209
- const box = this.getElemByRef(BOX);
210
- const item = this.getElemByRef(this.selected || '');
226
+ const box = this.getElemByRef(BOX) as HTMLElement;
227
+ const item = this.getElemByRef(this.selected || '') as HTMLElement;
211
228
 
212
229
  if (box && item && item.className.includes(CLASS.item)) {
213
230
  const boxRect = box.getClientRects()[0];
@@ -229,13 +246,14 @@ export default Vue.extend({
229
246
  */
230
247
  toggleError(type: Error, val: boolean, refId?: string) {
231
248
  this.errors[type] = val;
249
+
232
250
  if (refId) {
233
251
  this.toggleErrorClass(refId, val);
234
252
  }
235
253
  },
236
254
 
237
255
  toggleErrorClass(refId: string, val: boolean) {
238
- const input = this.getElemByRef(refId)?.$el;
256
+ const input = (this.getElemByRef(refId) as Vue)?.$el;
239
257
 
240
258
  if (input) {
241
259
  if (val) {
@@ -250,7 +268,11 @@ export default Vue.extend({
250
268
  * Show/Hide the input line to create new item
251
269
  */
252
270
  toggleCreateMode(show: boolean) {
271
+ if (this.readonly) {
272
+ return;
273
+ }
253
274
  if (show) {
275
+ this.toggleEditMode(false);
254
276
  this.value = '';
255
277
 
256
278
  this.isCreateItem = true;
@@ -268,31 +290,34 @@ export default Vue.extend({
268
290
  * Show/Hide the in-line editing to edit an existing item
269
291
  */
270
292
  toggleEditMode(show: boolean, item?: string) {
293
+ if (this.readonly) {
294
+ return;
295
+ }
271
296
  if (show) {
272
297
  this.toggleCreateMode(false);
273
- this.value = this.isEditItem;
298
+ this.value = this.editedItem;
274
299
 
275
- this.isEditItem = item || '';
300
+ this.editedItem = item || '';
276
301
  this.setFocus(INPUT.edit);
277
302
  } else {
278
303
  this.value = null;
279
304
  this.toggleError('duplicate', false);
280
305
  this.onSelectLeave();
281
306
 
282
- this.isEditItem = null;
307
+ this.editedItem = null;
283
308
  }
284
309
  },
285
310
 
286
311
  getElemByRef(id: string) {
287
312
  const ref = this.$refs[id];
288
313
 
289
- return (Array.isArray(ref) ? ref[0] : ref) as any;
314
+ return Array.isArray(ref) ? ref[0] : ref;
290
315
  },
291
316
 
292
317
  /**
293
318
  * Create a new item and insert in the items list
294
319
  */
295
- saveItem() {
320
+ saveItem(closeInput = true) {
296
321
  const value = this.value?.trim();
297
322
 
298
323
  if (value) {
@@ -301,21 +326,20 @@ export default Vue.extend({
301
326
  value,
302
327
  ];
303
328
 
304
- if (hasDuplicatedStrings(items, this.caseSensitive)) {
305
- this.toggleError('duplicate', true, INPUT.create);
306
-
307
- return;
329
+ if (!hasDuplicatedStrings(items, this.caseSensitive)) {
330
+ this.updateItems(items);
308
331
  }
332
+ }
309
333
 
310
- this.updateItems(items);
334
+ if (closeInput) {
335
+ this.toggleCreateMode(false);
311
336
  }
312
- this.toggleCreateMode(false);
313
337
  },
314
338
 
315
339
  /**
316
340
  * Update an existing item in the items list
317
341
  */
318
- updateItem(item: string) {
342
+ updateItem(item: string, closeInput = true) {
319
343
  const value = this.value?.trim();
320
344
 
321
345
  if (value) {
@@ -326,22 +350,21 @@ export default Vue.extend({
326
350
  items[index] = value;
327
351
  }
328
352
 
329
- if (hasDuplicatedStrings(items, this.caseSensitive)) {
330
- this.toggleError('duplicate', true, INPUT.edit);
331
-
332
- return;
353
+ if (!hasDuplicatedStrings(items, this.caseSensitive)) {
354
+ this.updateItems(items);
333
355
  }
356
+ }
334
357
 
335
- this.updateItems(items);
358
+ if (closeInput) {
359
+ this.toggleEditMode(false);
336
360
  }
337
- this.toggleEditMode(false);
338
361
  },
339
362
 
340
363
  /**
341
364
  * Remove an item from items list
342
365
  */
343
366
  deleteItem(item?: string) {
344
- const items = this.items.filter(f => f !== item);
367
+ const items = this.items.filter((f) => f !== item);
345
368
 
346
369
  this.updateItems(items);
347
370
  },
@@ -387,19 +410,20 @@ export default Vue.extend({
387
410
  @blur="onSelectLeave(item)"
388
411
  >
389
412
  <span
390
- v-if="!isEditItem || isEditItem !== item"
413
+ v-if="!editedItem || editedItem !== item"
391
414
  class="label static"
392
415
  >
393
416
  {{ item }}
394
417
  </span>
395
418
  <LabeledInput
396
- v-if="isEditItem && isEditItem === item"
419
+ v-if="editedItem && editedItem === item"
397
420
  ref="item-edit"
421
+ :data-testid="`item-edit-${item}`"
398
422
  class="edit-input static"
399
423
  :value="value != null ? value : item"
400
424
  @input="onChange($event)"
401
- @blur.prevent="toggleEditMode(false)"
402
- @keydown.native.enter="updateItem(item)"
425
+ @blur.prevent="updateItem(item)"
426
+ @keydown.native.enter="updateItem(item, !errors.duplicate)"
403
427
  />
404
428
  </div>
405
429
  <div
@@ -408,12 +432,14 @@ export default Vue.extend({
408
432
  >
409
433
  <LabeledInput
410
434
  ref="item-create"
435
+ data-testid="item-create"
411
436
  class="create-input static"
412
437
  type="text"
413
438
  :value="value"
414
439
  :placeholder="placeholder"
415
440
  @input="onChange($event)"
416
- @keydown.native.enter="saveItem"
441
+ @blur.prevent="saveItem"
442
+ @keydown.native.enter="saveItem(!errors.duplicate)"
417
443
  />
418
444
  </div>
419
445
  </div>
@@ -427,25 +453,32 @@ export default Vue.extend({
427
453
  class="action-buttons"
428
454
  >
429
455
  <button
456
+ data-testid="button-remove"
430
457
  class="btn btn-sm role-tertiary remove-button"
431
- :disabled="!selected && !isCreateItem && !isEditItem"
458
+ :disabled="!selected && !isCreateItem && !editedItem"
432
459
  @mousedown.prevent="onClickMinusButton"
433
460
  >
434
461
  <span class="icon icon-minus icon-sm" />
435
462
  </button>
436
463
  <button
464
+ data-testid="button-add"
437
465
  class="btn btn-sm role-tertiary add-button"
438
- :disabled="isCreateItem"
466
+ :disabled="isCreateItem || editedItem"
439
467
  @click.prevent="onClickPlusButton"
440
468
  >
441
469
  <span class="icon icon-plus icon-sm" />
442
470
  </button>
443
471
  </div>
444
472
  <div class="messages">
445
- <i v-if="errorMessagesArray.length > 0" class="icon icon-warning icon-lg" />
473
+ <i
474
+ v-if="errorMessagesArray.length > 0"
475
+ data-testid="i-warning-icon"
476
+ class="icon icon-warning icon-lg"
477
+ />
446
478
  <span
447
479
  v-for="(msg, idx) in errorMessagesArray"
448
480
  :key="idx"
481
+ :data-testid="`span-error-message-${msg}`"
449
482
  class="error"
450
483
  >
451
484
  {{ idx > 0 ? '; ' : '' }}
@@ -499,6 +532,7 @@ export default Vue.extend({
499
532
  width: auto;
500
533
  user-select: none;
501
534
  overflow: hidden;
535
+ white-space: no-wrap;
502
536
  text-overflow: ellipsis;
503
537
  padding-top: 1px;
504
538
  }
File without changes
package/store/prefs.js CHANGED
@@ -1,7 +1,7 @@
1
- import Vue from 'vue';
1
+ import { SETTING } from '@shell/config/settings';
2
2
  import { MANAGEMENT, STEVE } from '@shell/config/types';
3
3
  import { clone } from '@shell/utils/object';
4
- import { SETTING } from '@shell/config/settings';
4
+ import Vue from 'vue';
5
5
 
6
6
  const definitions = {};
7
7
  /**
@@ -115,8 +115,7 @@ export const PROVISIONER = create('provisioner', _RKE2, { options: [_RKE1, _RKE2
115
115
  export const PSP_DEPRECATION_BANNER = create('hide-psp-deprecation-banner', false, { parseJSON });
116
116
 
117
117
  // Maximum number of clusters to show in the slide-in menu
118
- export const MENU_MAX_CLUSTERS = create('menu-max-clusters', 4, { options: [2, 3, 4, 5, 6, 7, 8, 9, 10], parseJSON });
119
-
118
+ export const MENU_MAX_CLUSTERS = 10;
120
119
  // Prompt for confirm when scaling down node pool in GUI and save the pref
121
120
  export const SCALE_POOL_PROMPT = create('scale-pool-prompt', null, { parseJSON });
122
121
  // --------------------
package/store/type-map.js CHANGED
@@ -593,18 +593,8 @@ export const getters = {
593
593
  }
594
594
 
595
595
  const label = typeObj.labelKey ? rootGetters['i18n/t'](typeObj.labelKey) || typeObj.label : typeObj.label;
596
- const virtual = !!typeObj.virtual;
597
- let icon = typeObj.icon;
598
596
 
599
- if ( (!virtual || typeObj.isSpoofed ) && !icon ) {
600
- if ( namespaced ) {
601
- icon = 'folder';
602
- } else {
603
- icon = 'globe';
604
- }
605
- }
606
-
607
- const labelDisplay = highlightLabel(label, icon, typeObj.count, typeObj.schema);
597
+ const labelDisplay = highlightLabel(label, typeObj.count, typeObj.schema);
608
598
 
609
599
  if ( !labelDisplay ) {
610
600
  // Search happens in highlight and returns null if not found
@@ -711,7 +701,7 @@ export const getters = {
711
701
  return group;
712
702
  }
713
703
 
714
- function highlightLabel(original, icon, count, schema) {
704
+ function highlightLabel(original, count, schema) {
715
705
  let label = escapeHtml(original);
716
706
 
717
707
  if ( searchRegex ) {
@@ -735,10 +725,6 @@ export const getters = {
735
725
  }
736
726
  }
737
727
 
738
- if ( icon ) {
739
- label = `<i class="icon icon-fw icon-${ icon }"></i>${ label }`;
740
- }
741
-
742
728
  return label;
743
729
  }
744
730
  };
@@ -2868,7 +2868,7 @@ export const _RKE1: "rke1";
2868
2868
  export const _RKE2: "rke2";
2869
2869
  export const PROVISIONER: any;
2870
2870
  export const PSP_DEPRECATION_BANNER: any;
2871
- export const MENU_MAX_CLUSTERS: any;
2871
+ export const MENU_MAX_CLUSTERS: 10;
2872
2872
  export const SCALE_POOL_PROMPT: any;
2873
2873
  export function state(): {
2874
2874
  cookiesLoaded: boolean;
@@ -3605,35 +3605,35 @@ export namespace KEY {
3605
3605
  }
3606
3606
  }
3607
3607
 
3608
- // @shell/utils/poller
3608
+ // @shell/utils/poller-sequential
3609
3609
 
3610
- declare module '@shell/utils/poller' {
3611
- export default class Poller {
3610
+ declare module '@shell/utils/poller-sequential' {
3611
+ export default class PollerSequential {
3612
3612
  constructor(fn: any, pollRateMs: any, maxRetries?: number);
3613
3613
  fn: any;
3614
3614
  pollRateMs: any;
3615
3615
  maxRetries: number;
3616
- intervalId: any;
3616
+ timeoutId: any;
3617
3617
  tryCount: number;
3618
3618
  start(): void;
3619
3619
  stop(): void;
3620
+ _poll(): void;
3620
3621
  _intervalMethod(): Promise<void>;
3621
3622
  }
3622
3623
  }
3623
3624
 
3624
- // @shell/utils/poller-sequential
3625
+ // @shell/utils/poller
3625
3626
 
3626
- declare module '@shell/utils/poller-sequential' {
3627
- export default class PollerSequential {
3627
+ declare module '@shell/utils/poller' {
3628
+ export default class Poller {
3628
3629
  constructor(fn: any, pollRateMs: any, maxRetries?: number);
3629
3630
  fn: any;
3630
3631
  pollRateMs: any;
3631
3632
  maxRetries: number;
3632
- timeoutId: any;
3633
+ intervalId: any;
3633
3634
  tryCount: number;
3634
3635
  start(): void;
3635
3636
  stop(): void;
3636
- _poll(): void;
3637
3637
  _intervalMethod(): Promise<void>;
3638
3638
  }
3639
3639
  }
@@ -3908,6 +3908,9 @@ export function splitObjectPath(path: any): any;
3908
3908
  export function joinObjectPath(ary: any): string;
3909
3909
  export function shortenedImage(image: any): any;
3910
3910
  export function isIpv4(ip: any): boolean;
3911
+ export function sanitizeKey(k: any): any;
3912
+ export function sanitizeValue(v: any): any;
3913
+ export function sanitizeIP(v: any): any;
3911
3914
  export namespace CHARSET {
3912
3915
  export { num as NUMERIC };
3913
3916
  export const NO_VOWELS: string;
@@ -0,0 +1,61 @@
1
+ import { sortBy } from '@shell/utils/sort';
2
+
3
+ describe('fx: sort', () => {
4
+ describe('sortBy', () => {
5
+ const testSortBy = <T = object[]>(ary: T[], key: string[], expected: T[], desc?: boolean) => {
6
+ const result = sortBy(ary, key, desc);
7
+
8
+ expect(result).toStrictEqual(expected);
9
+ };
10
+
11
+ it.each([
12
+ [[{ a: 1 }, { a: 9 }], ['a'], [{ a: 1 }, { a: 9 }]],
13
+ [[{ a: 2 }, { a: 1 }], ['a'], [{ a: 1 }, { a: 2 }]],
14
+ ])('should sort by single property', (ary, key, expected) => {
15
+ testSortBy(ary, key, expected);
16
+ });
17
+
18
+ it.each([
19
+ [[{ a: 1, b: 1 }, { a: 9, b: 9 }], ['a', 'b'], [{ a: 1, b: 1 }, { a: 9, b: 9 }]],
20
+ [[{ a: 2, b: 2 }, { a: 1, b: 1 }], ['a', 'b'], [{ a: 1, b: 1 }, { a: 2, b: 2 }]],
21
+ [[{ a: 2, b: 1 }, { a: 1, b: 9 }], ['a', 'b'], [{ a: 1, b: 9 }, { a: 2, b: 1 }]],
22
+ [[{ a: 1, b: 2 }, { a: 9, b: 1 }], ['a', 'b'], [{ a: 1, b: 2 }, { a: 9, b: 1 }]],
23
+ [[{ a: 1, b: 1 }, { a: 9, b: 9 }], ['a', 'b'], [{ a: 1, b: 1 }, { a: 9, b: 9 }]],
24
+ ])('should sort by two properties (primary property always first)', (ary, key, expected) => {
25
+ testSortBy(ary, key, expected);
26
+ });
27
+
28
+ it.each([
29
+ [[{ a: 1, b: 1 }, { a: 1, b: 9 }], ['a', 'b'], [{ a: 1, b: 1 }, { a: 1, b: 9 }]],
30
+ [[{ a: 1, b: 2 }, { a: 1, b: 1 }], ['a', 'b'], [{ a: 1, b: 1 }, { a: 1, b: 2 }]],
31
+ ])('should sort by two properties (primary property the same)', (ary, key, expected) => {
32
+ testSortBy(ary, key, expected);
33
+ });
34
+
35
+ describe('descending', () => {
36
+ it.each([
37
+ [[{ a: 1 }, { a: 9 }], ['a'], [{ a: 9 }, { a: 1 }]],
38
+ [[{ a: 2 }, { a: 1 }], ['a'], [{ a: 2 }, { a: 1 }]],
39
+ ])('should sort by single property', (ary, key, expected) => {
40
+ testSortBy(ary, key, expected, true);
41
+ });
42
+
43
+ it.each([
44
+ [[{ a: 1, b: 1 }, { a: 9, b: 9 }], ['a', 'b'], [{ a: 9, b: 9 }, { a: 1, b: 1 }]],
45
+ [[{ a: 2, b: 2 }, { a: 1, b: 1 }], ['a', 'b'], [{ a: 2, b: 2 }, { a: 1, b: 1 }]],
46
+ [[{ a: 2, b: 1 }, { a: 1, b: 9 }], ['a', 'b'], [{ a: 2, b: 1 }, { a: 1, b: 9 }]],
47
+ [[{ a: 1, b: 2 }, { a: 9, b: 1 }], ['a', 'b'], [{ a: 9, b: 1 }, { a: 1, b: 2 }]],
48
+ [[{ a: 1, b: 1 }, { a: 9, b: 9 }], ['a', 'b'], [{ a: 9, b: 9 }, { a: 1, b: 1 }]],
49
+ ])('should sort by two properties', (ary, key, expected) => {
50
+ testSortBy(ary, key, expected, true);
51
+ });
52
+
53
+ it.each([
54
+ [[{ a: 1, b: 1 }, { a: 1, b: 9 }], ['a', 'b'], [{ a: 1, b: 9 }, { a: 1, b: 1 }]],
55
+ [[{ a: 1, b: 2 }, { a: 1, b: 1 }], ['a', 'b'], [{ a: 1, b: 2 }, { a: 1, b: 1 }]],
56
+ ])('should sort by two properties (primary property the same)', (ary, key, expected) => {
57
+ testSortBy(ary, key, expected, true);
58
+ });
59
+ });
60
+ });
61
+ });
package/utils/string.js CHANGED
@@ -309,3 +309,15 @@ export function isIpv4(ip) {
309
309
 
310
310
  return reg.test(ip);
311
311
  }
312
+
313
+ export function sanitizeKey(k) {
314
+ return (k || '').replace(/[^a-z0-9./_-]/ig, '');
315
+ }
316
+
317
+ export function sanitizeValue(v) {
318
+ return (v || '').replace(/[^a-z0-9._-]/ig, '');
319
+ }
320
+
321
+ export function sanitizeIP(v) {
322
+ return (v || '').replace(/[^a-z0-9.:_-]/ig, '');
323
+ }
package/vue.config.js CHANGED
@@ -250,10 +250,6 @@ module.exports = function(dir, _appConfig) {
250
250
  console.log(`Version: ${ dashboardVersion }`); // eslint-disable-line no-console
251
251
  }
252
252
 
253
- if ( !dev ) {
254
- console.log(`Version: ${ dashboardVersion }`); // eslint-disable-line no-console
255
- }
256
-
257
253
  if ( resourceBase ) {
258
254
  console.log(`Resource Base URL: ${ resourceBase }`); // eslint-disable-line no-console
259
255
  }
@@ -410,6 +406,7 @@ module.exports = function(dir, _appConfig) {
410
406
  rancherEnv,
411
407
  dashboardVersion
412
408
  }),
409
+
413
410
  }));
414
411
 
415
412
  // The static assets need to be in the built assets directory in order to get served (primarily the favicon)