@rancher/shell 0.3.28 → 0.4.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 (72) hide show
  1. package/.DS_Store +0 -0
  2. package/assets/translations/en-us.yaml +16 -2
  3. package/assets/translations/zh-hans.yaml +1 -1
  4. package/chart/monitoring/grafana/index.vue +2 -2
  5. package/components/AsyncButton.vue +9 -0
  6. package/components/CopyCode.vue +6 -2
  7. package/components/CopyToClipboard.vue +2 -1
  8. package/components/CopyToClipboardText.vue +14 -9
  9. package/components/EtcdInfoBanner.vue +4 -4
  10. package/components/Markdown.vue +16 -12
  11. package/components/ResourceDetail/Masthead.vue +9 -6
  12. package/components/SortableTable/THead.vue +7 -9
  13. package/components/SortableTable/index.vue +1 -2
  14. package/components/StatusTable.vue +5 -1
  15. package/components/__tests__/CopyCode.test.ts +5 -4
  16. package/components/fleet/FleetBundles.vue +5 -11
  17. package/components/fleet/FleetStatus.vue +3 -3
  18. package/components/fleet/FleetSummary.vue +35 -30
  19. package/components/fleet/__tests__/FleetSummary.test.ts +316 -0
  20. package/components/form/Password.vue +3 -1
  21. package/components/nav/Header.vue +1 -1
  22. package/config/home-links.js +1 -1
  23. package/core/plugin-helpers.js +3 -5
  24. package/creators/app/files/.gitlab-ci.yml +14 -0
  25. package/creators/app/init +19 -0
  26. package/detail/provisioning.cattle.io.cluster.vue +2 -1
  27. package/edit/monitoring.coreos.com.prometheusrule/AlertingRule.vue +12 -3
  28. package/edit/monitoring.coreos.com.prometheusrule/GroupRules.vue +2 -1
  29. package/edit/provisioning.cattle.io.cluster/__tests__/CustomCommand.tests.ts +3 -1
  30. package/edit/workload/Upgrading.vue +3 -2
  31. package/edit/workload/index.vue +2 -1
  32. package/edit/workload/storage/persistentVolumeClaim/persistentvolumeclaim.vue +2 -1
  33. package/initialize/index.js +24 -5
  34. package/machine-config/__tests__/vmwarevsphere.test.ts +72 -0
  35. package/machine-config/vmwarevsphere.vue +68 -13
  36. package/models/__tests__/management.cattle.io.cluster.test.ts +4 -0
  37. package/models/management.cattle.io.cluster.js +7 -3
  38. package/models/provisioning.cattle.io.cluster.js +19 -1
  39. package/package.json +3 -2
  40. package/pages/c/_cluster/apps/charts/index.vue +64 -43
  41. package/plugins/clean-html-directive.js +1 -19
  42. package/plugins/clean-html.js +53 -0
  43. package/plugins/clean-tooltip-directive.js +1 -1
  44. package/plugins/index.js +11 -0
  45. package/rancher-components/BadgeState/BadgeState.vue +5 -1
  46. package/rancher-components/Banner/Banner.test.ts +51 -1
  47. package/rancher-components/Banner/Banner.vue +134 -53
  48. package/rancher-components/Card/Card.test.ts +37 -0
  49. package/rancher-components/Card/Card.vue +24 -7
  50. package/rancher-components/Form/Checkbox/Checkbox.test.ts +20 -29
  51. package/rancher-components/Form/Checkbox/Checkbox.vue +45 -20
  52. package/rancher-components/Form/LabeledInput/LabeledInput.test.ts +2 -8
  53. package/rancher-components/Form/LabeledInput/LabeledInput.vue +22 -10
  54. package/rancher-components/Form/Radio/RadioButton.test.ts +31 -0
  55. package/rancher-components/Form/Radio/RadioButton.vue +30 -13
  56. package/rancher-components/Form/Radio/RadioGroup.vue +26 -7
  57. package/rancher-components/Form/TextArea/TextAreaAutoGrow.vue +7 -6
  58. package/rancher-components/Form/ToggleSwitch/ToggleSwitch.test.ts +25 -38
  59. package/rancher-components/Form/ToggleSwitch/ToggleSwitch.vue +23 -11
  60. package/rancher-components/LabeledTooltip/LabeledTooltip.vue +19 -5
  61. package/rancher-components/StringList/StringList.test.ts +453 -49
  62. package/rancher-components/StringList/StringList.vue +92 -58
  63. package/scripts/.DS_Store +0 -0
  64. package/scripts/extension/bundle +19 -7
  65. package/scripts/extension/helm/scripts/package +11 -3
  66. package/scripts/extension/publish +20 -9
  67. package/scripts/verdaccio.log +205 -0
  68. package/store/index.js +3 -4
  69. package/types/shell/index.d.ts +15 -9
  70. package/utils/clipboard.js +5 -0
  71. package/yarn-error.log +200 -0
  72. package/plugins/vue-clipboard2.js +0 -4
@@ -39,7 +39,17 @@ const OS_OPTIONS = [
39
39
  'linux',
40
40
  'windows'
41
41
  ];
42
- const DEFAULT_CFGPARAM = ['disk.enableUUID=TRUE'];
42
+
43
+ export const DEFAULT_VALUES = {
44
+ cpuCount: '2',
45
+ diskSize: '20000',
46
+ memorySize: '4096',
47
+ hostsystem: '',
48
+ cloudConfig: '#cloud-config\n\n',
49
+ gracefulShutdownTimeout: '0',
50
+ cfgparam: ['disk.enableUUID=TRUE'],
51
+ os: OS_OPTIONS[0]
52
+ };
43
53
 
44
54
  const getDefaultVappOptions = (networks) => {
45
55
  return {
@@ -217,15 +227,27 @@ export default {
217
227
  if (this.mode === _CREATE && !this.value.initted) {
218
228
  Object.defineProperty(this.value, 'initted', { value: true, enumerable: false });
219
229
 
230
+ const {
231
+ cpuCount,
232
+ diskSize,
233
+ memorySize,
234
+ hostsystem,
235
+ cloudConfig,
236
+ gracefulShutdownTimeout,
237
+ cfgparam,
238
+ os
239
+ } = DEFAULT_VALUES;
240
+
220
241
  set(this.value, 'creationType', creationMethods[0].value);
221
- set(this.value, 'cpuCount', '2');
222
- set(this.value, 'diskSize', '20000');
223
- set(this.value, 'memorySize', '4096');
224
- set(this.value, 'hostsystem', '');
225
- set(this.value, 'cloudConfig', '#cloud-config\n\n');
226
- set(this.value, 'cfgparam', DEFAULT_CFGPARAM);
242
+ set(this.value, 'cpuCount', cpuCount);
243
+ set(this.value, 'diskSize', diskSize);
244
+ set(this.value, 'memorySize', memorySize);
245
+ set(this.value, 'hostsystem', hostsystem);
246
+ set(this.value, 'gracefulShutdownTimeout', gracefulShutdownTimeout);
247
+ set(this.value, 'cloudConfig', cloudConfig);
248
+ set(this.value, 'cfgparam', cfgparam);
227
249
  set(this.value, 'vappProperty', this.value.vappProperty);
228
- set(this.value, 'os', OS_OPTIONS[0]);
250
+ set(this.value, 'os', os);
229
251
  Object.entries(INITIAL_VAPP_OPTIONS).forEach(([key, value]) => {
230
252
  set(this.value, key, value);
231
253
  });
@@ -326,6 +348,8 @@ export default {
326
348
  memorySize: integerString('value.memorySize'),
327
349
  diskSize: integerString('value.diskSize'),
328
350
 
351
+ gracefulShutdownTimeout: integerString('value.gracefulShutdownTimeout'),
352
+
329
353
  showCloudConfigYaml() {
330
354
  return this.value.creationType !== 'legacy';
331
355
  },
@@ -731,7 +755,10 @@ export default {
731
755
  </p>
732
756
  </h4>
733
757
  <div slot="body">
734
- <div class="row">
758
+ <div
759
+ class="row"
760
+ data-testid="datacenter"
761
+ >
735
762
  <div class="col span-6">
736
763
  <LabeledSelect
737
764
  v-model="value.datacenter"
@@ -742,7 +769,10 @@ export default {
742
769
  :disabled="disabled"
743
770
  />
744
771
  </div>
745
- <div class="col span-6">
772
+ <div
773
+ class="col span-6"
774
+ data-testid="resourcePool"
775
+ >
746
776
  <LabeledSelect
747
777
  v-model="value.pool"
748
778
  :loading="resourcePoolsLoading"
@@ -754,7 +784,10 @@ export default {
754
784
  </div>
755
785
  </div>
756
786
  <div class="row mt-10">
757
- <div class="col span-6">
787
+ <div
788
+ class="col span-6"
789
+ data-testid="dataStore"
790
+ >
758
791
  <LabeledSelect
759
792
  v-model="value.datastore"
760
793
  :loading="dataStoresLoading"
@@ -764,7 +797,10 @@ export default {
764
797
  :disabled="disabled"
765
798
  />
766
799
  </div>
767
- <div class="col span-6">
800
+ <div
801
+ class="col span-6"
802
+ data-testid="folder"
803
+ >
768
804
  <LabeledSelect
769
805
  v-model="value.folder"
770
806
  :loading="foldersLoading"
@@ -776,7 +812,10 @@ export default {
776
812
  </div>
777
813
  </div>
778
814
  <div class="row mt-10">
779
- <div class="col span-12">
815
+ <div
816
+ class="col span-6"
817
+ data-testid="host"
818
+ >
780
819
  <LabeledSelect
781
820
  v-model="host"
782
821
  :loading="hostsLoading"
@@ -789,6 +828,22 @@ export default {
789
828
  {{ t('cluster.machineConfig.vsphere.scheduling.host.note') }}
790
829
  </p>
791
830
  </div>
831
+ <div
832
+ class="col span-6"
833
+ data-testid="gracefulShutdownTimeout"
834
+ >
835
+ <UnitInput
836
+ v-model="gracefulShutdownTimeout"
837
+ :mode="mode"
838
+ :label="t('cluster.machineConfig.vsphere.scheduling.gracefulShutdownTimeout.label')"
839
+ :suffix="t('suffix.seconds', { count: gracefulShutdownTimeout})"
840
+ :disabled="disabled"
841
+ min="0"
842
+ />
843
+ <p class="text-muted mt-5">
844
+ {{ t('cluster.machineConfig.vsphere.scheduling.gracefulShutdownTimeout.note') }}
845
+ </p>
846
+ </div>
792
847
  </div>
793
848
  </div>
794
849
  </Card>
@@ -1,5 +1,9 @@
1
1
  import MgmtCluster from '@shell/models/management.cattle.io.cluster';
2
2
 
3
+ jest.mock('@shell/utils/clipboard', () => {
4
+ return { copyTextToClipboard: jest.fn(() => Promise.resolve({})) };
5
+ });
6
+
3
7
  describe('class MgmtCluster', () => {
4
8
  describe('provisioner', () => {
5
9
  const testCases = [
@@ -1,4 +1,3 @@
1
- import Vue from 'vue';
2
1
  import { CATALOG, CLUSTER_BADGE } from '@shell/config/labels-annotations';
3
2
  import { NODE, FLEET, MANAGEMENT, CAPI } from '@shell/config/types';
4
3
  import { insertAt, addObject, removeObject } from '@shell/utils/array';
@@ -15,6 +14,7 @@ import HybridModel from '@shell/plugins/steve/hybrid-class';
15
14
  import { LINUX, WINDOWS } from '@shell/store/catalog';
16
15
  import { KONTAINER_TO_DRIVER } from './management.cattle.io.kontainerdriver';
17
16
  import { PINNED_CLUSTERS } from '@shell/store/prefs';
17
+ import { copyTextToClipboard } from '@shell/utils/clipboard';
18
18
 
19
19
  // See translation file cluster.providers for list of providers
20
20
  // If the logo is not named with the provider name, add an override here
@@ -408,9 +408,13 @@ export default class MgmtCluster extends HybridModel {
408
408
  }
409
409
 
410
410
  async copyKubeConfig() {
411
- const config = await this.generateKubeConfig();
411
+ try {
412
+ const config = await this.generateKubeConfig();
412
413
 
413
- Vue.prototype.$copyText(config);
414
+ if (config) {
415
+ await copyTextToClipboard(config);
416
+ }
417
+ } catch {}
414
418
  }
415
419
 
416
420
  async fetchNodeMetrics() {
@@ -1,5 +1,5 @@
1
1
  import {
2
- CAPI, MANAGEMENT, NORMAN, SNAPSHOT, HCI
2
+ CAPI, MANAGEMENT, NAMESPACE, NORMAN, SNAPSHOT, HCI, LOCAL_CLUSTER
3
3
  } from '@shell/config/types';
4
4
  import SteveModel from '@shell/plugins/steve/steve-class';
5
5
  import { findBy } from '@shell/utils/array';
@@ -883,4 +883,22 @@ export default class ProvCluster extends SteveModel {
883
883
  get hasError() {
884
884
  return this.status?.conditions?.some((condition) => condition.error === true);
885
885
  }
886
+
887
+ get namespaceLocation() {
888
+ const localCluster = this.$rootGetters['management/byId'](MANAGEMENT.CLUSTER, LOCAL_CLUSTER);
889
+
890
+ if (localCluster) {
891
+ return {
892
+ name: 'c-cluster-product-resource-id',
893
+ params: {
894
+ cluster: localCluster.id,
895
+ product: this.$rootGetters['productId'],
896
+ resource: NAMESPACE,
897
+ id: this.namespace
898
+ }
899
+ };
900
+ }
901
+
902
+ return null;
903
+ }
886
904
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rancher/shell",
3
- "version": "0.3.28",
3
+ "version": "0.4.0",
4
4
  "description": "Rancher Dashboard Shell",
5
5
  "repository": "https://github.com/rancherlabs/dashboard",
6
6
  "license": "Apache-2.0",
@@ -41,6 +41,7 @@
41
41
  "@nuxtjs/eslint-config-typescript": "6.0.1",
42
42
  "@nuxtjs/webpack-profile": "0.1.0",
43
43
  "@popperjs/core": "2.4.4",
44
+ "@types/is-url": "1.2.30",
44
45
  "@types/node": "16.4.3",
45
46
  "@typescript-eslint/eslint-plugin": "4.33.0",
46
47
  "@typescript-eslint/parser": "4.33.0",
@@ -119,7 +120,7 @@
119
120
  "url-parse": "1.5.10",
120
121
  "v-tooltip": "2.0.3",
121
122
  "vue": "2.7.14",
122
- "vue-clipboard2": "0.3.1",
123
+ "clipboard-polyfill": "4.0.1",
123
124
  "vue-codemirror": "4.0.6",
124
125
  "vue-js-modal": "1.3.35",
125
126
  "vue-resize": "0.4.5",
@@ -138,6 +138,11 @@ export default {
138
138
  return reducedRepos;
139
139
  },
140
140
 
141
+ /**
142
+ * Filter allll charts by invalid entries (deprecated, hidden and ui plugin).
143
+ *
144
+ * This does not include any user provided filters (like selected repos, categories and text query)
145
+ */
141
146
  enabledCharts() {
142
147
  return (this.allCharts || []).filter((c) => {
143
148
  if ( c.deprecated && !this.showDeprecated ) {
@@ -148,10 +153,6 @@ export default {
148
153
  return false;
149
154
  }
150
155
 
151
- if ( this.hideRepos.includes(c.repoKey) ) {
152
- return false;
153
- }
154
-
155
156
  if (isUIPlugin(c)) {
156
157
  return false;
157
158
  }
@@ -160,26 +161,28 @@ export default {
160
161
  });
161
162
  },
162
163
 
164
+ /**
165
+ * Filter enabled charts allll filters. These are what the user will see in the list
166
+ */
163
167
  filteredCharts() {
164
- const enabledCharts = (this.enabledCharts || []);
165
- const clusterProvider = this.currentCluster.status.provider || 'other';
166
-
167
- return filterAndArrangeCharts(enabledCharts, {
168
- clusterProvider,
169
- category: this.category,
170
- searchQuery: this.searchQuery,
171
- showDeprecated: this.showDeprecated,
172
- showHidden: this.showHidden,
173
- hideRepos: this.hideRepos,
174
- hideTypes: [CATALOG._CLUSTER_TPL],
175
- showPrerelease: this.$store.getters['prefs/get'](SHOW_PRE_RELEASE),
168
+ return this.filterCharts({
169
+ category: this.category,
170
+ searchQuery: this.searchQuery,
171
+ hideRepos: this.hideRepos
176
172
  });
177
173
  },
178
174
 
179
- getFeaturedCharts() {
180
- const allCharts = (this.filteredCharts || []);
175
+ /**
176
+ * Filter valid charts (alll filters minus user provided ones) by whether they are featured or not
177
+ *
178
+ * This will power the carousel
179
+ */
180
+ featuredCharts() {
181
+ const filteredCharts = this.filterCharts({});
182
+
183
+ // debugger;
181
184
 
182
- const featuredCharts = allCharts.filter((value) => value.featured).sort((a, b) => a.featured - b.featured);
185
+ const featuredCharts = filteredCharts.filter((value) => value.featured).sort((a, b) => a.featured - b.featured);
183
186
 
184
187
  return featuredCharts.slice(0, 5);
185
188
  },
@@ -187,7 +190,13 @@ export default {
187
190
  categories() {
188
191
  const map = {};
189
192
 
190
- for ( const chart of this.enabledCharts ) {
193
+ // Filter charts by everything except itself
194
+ const charts = this.filterCharts({
195
+ searchQuery: this.searchQuery,
196
+ hideRepos: this.hideRepos
197
+ });
198
+
199
+ for ( const chart of charts ) {
191
200
  for ( const c of chart.categories ) {
192
201
  if ( !map[c] ) {
193
202
  const labelKey = `catalog.charts.categories.${ lcFirst(c) }`;
@@ -208,14 +217,14 @@ export default {
208
217
  out.unshift({
209
218
  label: this.t('catalog.charts.categories.all'),
210
219
  value: '',
211
- count: this.enabledCharts.length
220
+ count: charts.length
212
221
  });
213
222
 
214
- return out;
223
+ return sortBy(out, ['label']);
215
224
  },
216
225
 
217
226
  showCarousel() {
218
- return this.chartMode === 'featured' && this.getFeaturedCharts.length;
227
+ return this.chartMode === 'featured' && this.featuredCharts.length;
219
228
  }
220
229
 
221
230
  },
@@ -334,6 +343,22 @@ export default {
334
343
  btnCb(false);
335
344
  }
336
345
  },
346
+
347
+ filterCharts({ category, searchQuery, hideRepos }) {
348
+ const enabledCharts = (this.enabledCharts || []);
349
+ const clusterProvider = this.currentCluster.status.provider || 'other';
350
+
351
+ return filterAndArrangeCharts(enabledCharts, {
352
+ clusterProvider,
353
+ category,
354
+ searchQuery,
355
+ showDeprecated: this.showDeprecated,
356
+ showHidden: this.showHidden,
357
+ hideRepos,
358
+ hideTypes: [CATALOG._CLUSTER_TPL],
359
+ showPrerelease: this.$store.getters['prefs/get'](SHOW_PRE_RELEASE),
360
+ });
361
+ }
337
362
  },
338
363
  };
339
364
  </script>
@@ -351,7 +376,7 @@ export default {
351
376
  </h1>
352
377
  </div>
353
378
  <div
354
- v-if="getFeaturedCharts.length > 0"
379
+ v-if="featuredCharts.length > 0"
355
380
  class="actions-container"
356
381
  >
357
382
  <ButtonGroup
@@ -363,7 +388,7 @@ export default {
363
388
  <div v-if="showCarousel">
364
389
  <h3>{{ t('catalog.charts.featuredCharts') }}</h3>
365
390
  <Carousel
366
- :sliders="getFeaturedCharts"
391
+ :sliders="featuredCharts"
367
392
  @clicked="(row) => selectChart(row)"
368
393
  />
369
394
  </div>
@@ -514,22 +539,21 @@ export default {
514
539
  }
515
540
  }
516
541
  }
517
- }
542
+ }
518
543
 
519
544
  .checkbox-select {
520
- .vs__search {
545
+ .vs__search {
521
546
  position: absolute;
522
547
  right: 0
523
548
  }
524
549
 
525
- .vs__selected-options {
550
+ .vs__selected-options {
526
551
  overflow: hidden;
527
552
  white-space: nowrap;
528
553
  text-overflow: ellipsis;
529
554
  display: inline-block;
530
555
  line-height: 2.4rem;
531
556
  }
532
-
533
557
  }
534
558
 
535
559
  .checkbox-outer-container.in-select {
@@ -537,7 +561,7 @@ export default {
537
561
  padding: 7px 0 6px 13px;
538
562
  width: calc(100% + 10px);
539
563
 
540
- ::v-deep.checkbox-label {
564
+ ::v-deep .checkbox-label {
541
565
  display: flex;
542
566
  align-items: center;
543
567
 
@@ -552,7 +576,7 @@ export default {
552
576
  }
553
577
  }
554
578
 
555
- &:hover ::v-deep.checkbox-label {
579
+ &:hover ::v-deep .checkbox-label {
556
580
  color: var(--body-text);
557
581
  }
558
582
 
@@ -560,7 +584,7 @@ export default {
560
584
  &:hover {
561
585
  background: var(--app-rancher-accent);
562
586
  }
563
- &:hover ::v-deep.checkbox-label {
587
+ &:hover ::v-deep .checkbox-label {
564
588
  color: var(--app-rancher-accent-text);
565
589
  }
566
590
  & i {
@@ -572,7 +596,7 @@ export default {
572
596
  &:hover {
573
597
  background: var(--app-partner-accent);
574
598
  }
575
- &:hover ::v-deep.checkbox-label {
599
+ &:hover ::v-deep .checkbox-label {
576
600
  color: var(--app-partner-accent-text);
577
601
  }
578
602
  & i {
@@ -584,7 +608,7 @@ export default {
584
608
  &:hover {
585
609
  background: var(--app-color1-accent);
586
610
  }
587
- &:hover ::v-deep.checkbox-label {
611
+ &:hover ::v-deep .checkbox-label {
588
612
  color: var(--app-color1-accent-text);
589
613
  }
590
614
  & i {
@@ -595,10 +619,10 @@ export default {
595
619
  &:hover {
596
620
  background: var(--app-color2-accent);
597
621
  }
598
- &:hover ::v-deep.checkbox-label {
622
+ &:hover ::v-deep .checkbox-label {
599
623
  color: var(--app-color2-accent-text);
600
624
  }
601
- & i {
625
+ & i {
602
626
  color: var(--app-color2-accent)
603
627
  }
604
628
  }
@@ -606,7 +630,7 @@ export default {
606
630
  &:hover {
607
631
  background: var(--app-color3-accent);
608
632
  }
609
- &:hover ::v-deep.checkbox-label {
633
+ &:hover ::v-deep .checkbox-label {
610
634
  color: var(--app-color3-accent-text);
611
635
  }
612
636
  & i {
@@ -628,7 +652,7 @@ export default {
628
652
  &:hover {
629
653
  background: var(--app-color5-accent);
630
654
  }
631
- &:hover ::v-deep.checkbox-label {
655
+ &:hover ::v-deep .checkbox-label {
632
656
  color: var(--app-color5-accent-text);
633
657
  }
634
658
  & i {
@@ -639,7 +663,7 @@ export default {
639
663
  &:hover {
640
664
  background: var(--app-color6-accent);
641
665
  }
642
- &:hover ::v-deep.checkbox-label {
666
+ &:hover ::v-deep .checkbox-label {
643
667
  color: var(--app-color6-accent-text);
644
668
  }
645
669
  & i {
@@ -650,7 +674,7 @@ export default {
650
674
  &:hover {
651
675
  background: var(--app-color7-accent);
652
676
  }
653
- &:hover ::v-deep.checkbox-label {
677
+ &:hover ::v-deep .checkbox-label {
654
678
  color: var(--app-color7-accent-text);
655
679
  }
656
680
  & i {
@@ -661,9 +685,6 @@ export default {
661
685
  &:hover {
662
686
  background: var(--app-color8-accent);
663
687
  }
664
- &:hover ::v-deep.checkbox-label {
665
- color: var(--app-color8-accent-text);
666
- }
667
688
  & i {
668
689
  color: var(--app-color8-accent)
669
690
  }
@@ -1,23 +1,5 @@
1
1
  import Vue from 'vue';
2
- import DOMPurify from 'dompurify';
3
-
4
- const ALLOWED_TAGS = [
5
- 'code',
6
- 'li',
7
- 'a',
8
- 'p',
9
- 'b',
10
- 'br',
11
- 'ul',
12
- 'pre',
13
- 'span',
14
- 'div',
15
- 'i',
16
- 'em',
17
- 'strong',
18
- ];
19
-
20
- export const purifyHTML = (value) => DOMPurify.sanitize(value, { ALLOWED_TAGS });
2
+ import { purifyHTML } from './clean-html';
21
3
 
22
4
  export const cleanHtmlDirective = {
23
5
  inserted(el, binding) {
@@ -0,0 +1,53 @@
1
+ import DOMPurify from 'dompurify';
2
+ import { uniq } from '@shell/utils/array';
3
+
4
+ const ALLOWED_TAGS = [
5
+ 'code',
6
+ 'li',
7
+ 'a',
8
+ 'p',
9
+ 'b',
10
+ 'br',
11
+ 'ul',
12
+ 'pre',
13
+ 'span',
14
+ 'div',
15
+ 'i',
16
+ 'em',
17
+ 'strong',
18
+ 'h1',
19
+ 'h2',
20
+ 'h3',
21
+ 'h4',
22
+ 'h5',
23
+ 'h6',
24
+ 'table',
25
+ 'thead',
26
+ 'tr',
27
+ 'th',
28
+ 'tbody',
29
+ 'td',
30
+ 'blockquote'
31
+ ];
32
+
33
+ // Allow 'A' tags to keep the target=_blank attribute if they have it
34
+ DOMPurify.addHook('uponSanitizeAttribute', (node, data) => {
35
+ if (node.tagName === 'A' && data.attrName === 'target' && data.attrValue === '_blank') {
36
+ data.forceKeepAttr = true;
37
+ }
38
+ });
39
+
40
+ // Ensure if an 'A' tag has target=_blank that we add noopener, noreferrer and nofollow to the 'rel' attribute
41
+ DOMPurify.addHook('afterSanitizeAttributes', (node) => {
42
+ if (node.tagName === 'A' && node?.target === '_blank') {
43
+ const rel = ['noopener', 'noreferrer', 'nofollow'];
44
+ const existingRel = node.rel?.length ? node.rel.split(' ') : [];
45
+ const combined = uniq([...rel, ...existingRel]);
46
+
47
+ node.setAttribute('rel', combined.join(' '));
48
+ }
49
+ });
50
+
51
+ export const purifyHTML = (value, options = { ALLOWED_TAGS }) => {
52
+ return DOMPurify.sanitize(value, options);
53
+ };
@@ -1,6 +1,6 @@
1
1
  import Vue from 'vue';
2
2
  import { VTooltip } from 'v-tooltip';
3
- import { purifyHTML } from './clean-html-directive';
3
+ import { purifyHTML } from './clean-html';
4
4
 
5
5
  function purifyContent(value) {
6
6
  const type = typeof value;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Load the directives
3
+ *
4
+ * These are included in a function that can be explictly called, so that we can be sure
5
+ * of the execution order, rather than importing them at the top of a file.
6
+ */
7
+ export function loadDirectives() {
8
+ import('./clean-html-directive');
9
+ import('./clean-tooltip-directive');
10
+ import('./directives');
11
+ }
@@ -60,7 +60,11 @@ export default Vue.extend({
60
60
 
61
61
  <template>
62
62
  <span :class="{'badge-state': true, [bg]: true}">
63
- <i v-if="icon" class="icon" :class="{[icon]: true, 'mr-5': !!msg}" />{{ msg }}
63
+ <i
64
+ v-if="icon"
65
+ class="icon"
66
+ :class="{[icon]: true, 'mr-5': !!msg}"
67
+ />{{ msg }}
64
68
  </span>
65
69
  </template>
66
70
 
@@ -1,13 +1,63 @@
1
1
  import { mount } from '@vue/test-utils';
2
2
  import { Banner } from './index';
3
+ import { cleanHtmlDirective } from '@shell/plugins/clean-html-directive';
3
4
 
4
5
  describe('component: Banner', () => {
5
6
  it('should display text based on label', () => {
6
7
  const label = 'test';
7
- const wrapper = mount(Banner, { propsData: { label } });
8
+ const wrapper = mount(
9
+ Banner,
10
+ {
11
+ directives: { cleanHtmlDirective },
12
+ propsData: { label }
13
+ });
8
14
 
9
15
  const element = wrapper.find('span').element;
10
16
 
11
17
  expect(element.textContent).toBe(label);
12
18
  });
19
+
20
+ it('should display an icon', () => {
21
+ const icon = 'my-icon';
22
+ const wrapper = mount(Banner, { propsData: { icon } });
23
+
24
+ const element = wrapper.find(`.${ icon }`).element;
25
+
26
+ expect(element.classList).toContain(icon);
27
+ });
28
+
29
+ it('should not display an icon', () => {
30
+ const wrapper = mount(Banner);
31
+
32
+ const element = wrapper.find(`[data-testid="banner-icon"]`).element;
33
+
34
+ expect(element).not.toBeDefined();
35
+ });
36
+
37
+ it('should emit close event', () => {
38
+ const wrapper = mount(Banner, { propsData: { closable: true } });
39
+ const element = wrapper.find(`[data-testid="banner-close"]`).element;
40
+
41
+ element.click();
42
+
43
+ expect(wrapper.emitted('close')).toHaveLength(1);
44
+ });
45
+
46
+ it('should add the right color', () => {
47
+ const color = 'red';
48
+ const wrapper = mount(Banner, { propsData: { color } });
49
+
50
+ const element = wrapper.element;
51
+
52
+ expect(element.classList).toContain(color);
53
+ });
54
+
55
+ it('should stack the banner messages', () => {
56
+ const stacked = true;
57
+ const wrapper = mount(Banner, { propsData: { stacked } });
58
+
59
+ const element = wrapper.find(`[data-testid="banner-content"]`).element;
60
+
61
+ expect(element.classList).toContain('stacked');
62
+ });
13
63
  });