@rancher/shell 0.3.29 → 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 (44) hide show
  1. package/.DS_Store +0 -0
  2. package/assets/translations/en-us.yaml +1 -1
  3. package/assets/translations/zh-hans.yaml +1 -1
  4. package/components/CopyCode.vue +6 -2
  5. package/components/CopyToClipboard.vue +2 -1
  6. package/components/CopyToClipboardText.vue +14 -9
  7. package/components/EtcdInfoBanner.vue +4 -4
  8. package/components/Markdown.vue +16 -12
  9. package/components/ResourceDetail/Masthead.vue +9 -6
  10. package/components/StatusTable.vue +5 -1
  11. package/components/__tests__/CopyCode.test.ts +5 -4
  12. package/components/fleet/FleetBundles.vue +5 -11
  13. package/components/fleet/FleetSummary.vue +3 -3
  14. package/components/fleet/__tests__/FleetSummary.test.ts +316 -0
  15. package/components/form/Password.vue +3 -1
  16. package/components/nav/Header.vue +1 -1
  17. package/config/home-links.js +1 -1
  18. package/core/plugin-helpers.js +3 -5
  19. package/creators/app/files/.gitlab-ci.yml +14 -0
  20. package/creators/app/init +19 -0
  21. package/edit/monitoring.coreos.com.prometheusrule/AlertingRule.vue +12 -3
  22. package/edit/monitoring.coreos.com.prometheusrule/GroupRules.vue +2 -1
  23. package/edit/provisioning.cattle.io.cluster/__tests__/CustomCommand.tests.ts +3 -1
  24. package/edit/workload/Upgrading.vue +3 -2
  25. package/edit/workload/storage/persistentVolumeClaim/persistentvolumeclaim.vue +2 -1
  26. package/initialize/index.js +24 -5
  27. package/models/__tests__/management.cattle.io.cluster.test.ts +4 -0
  28. package/models/management.cattle.io.cluster.js +7 -3
  29. package/models/provisioning.cattle.io.cluster.js +19 -1
  30. package/package.json +2 -2
  31. package/pages/c/_cluster/apps/charts/index.vue +64 -43
  32. package/plugins/clean-html-directive.js +1 -19
  33. package/plugins/clean-html.js +53 -0
  34. package/plugins/clean-tooltip-directive.js +1 -1
  35. package/plugins/index.js +11 -0
  36. package/scripts/.DS_Store +0 -0
  37. package/scripts/extension/bundle +19 -7
  38. package/scripts/extension/helm/scripts/package +11 -3
  39. package/scripts/extension/publish +20 -9
  40. package/scripts/verdaccio.log +205 -0
  41. package/store/index.js +3 -4
  42. package/types/shell/index.d.ts +6 -0
  43. package/utils/clipboard.js +5 -0
  44. package/plugins/vue-clipboard2.js +0 -4
@@ -2,6 +2,7 @@
2
2
  import { mapGetters } from 'vuex';
3
3
  import { LabeledInput } from '@components/Form/LabeledInput';
4
4
  import { CHARSET, randomStr } from '@shell/utils/string';
5
+ import { copyTextToClipboard } from '@shell/utils/clipboard';
5
6
 
6
7
  export default {
7
8
  components: { LabeledInput },
@@ -75,6 +76,7 @@ export default {
75
76
  }
76
77
  },
77
78
  methods: {
79
+ copyTextToClipboard,
78
80
  generatePassword() {
79
81
  this.password = randomStr(16, CHARSET.ALPHA_NUM);
80
82
  },
@@ -109,7 +111,7 @@ export default {
109
111
  >
110
112
  <a
111
113
  href="#"
112
- @click.prevent.stop="$copyText(password)"
114
+ @click.prevent.stop="copyTextToClipboard(password)"
113
115
  >{{ t('action.copy') }}</a>
114
116
  </div>
115
117
  <div
@@ -311,7 +311,7 @@ export default {
311
311
  product: this.currentProduct.name,
312
312
  cluster: this.currentCluster,
313
313
  };
314
- const enabled = action.enabled ? action.enabled.apply(this, [opts]) : true;
314
+ const enabled = action.enabled ? action.enabled.apply(this, [this.ctx]) : true;
315
315
 
316
316
  if (fn && enabled) {
317
317
  fn.apply(this, [opts, [], { $route: this.$route }]);
@@ -26,7 +26,7 @@ const DEFAULT_LINKS = [
26
26
  },
27
27
  {
28
28
  key: 'getStarted',
29
- value: 'https://ranchermanager.docs.rancher.com/getting-started/overview',
29
+ value: `${ DOCS_BASE }/getting-started/overview`,
30
30
  enabled: true,
31
31
  },
32
32
  ];
@@ -7,13 +7,11 @@ import {
7
7
  import { getProductFromRoute } from '@shell/middleware/authenticated';
8
8
  import { isEqual } from '@shell/utils/object';
9
9
 
10
- function checkRouteProduct({ name, params, query }, locationConfigParam) {
11
- const product = getProductFromRoute({
12
- name, params, query
13
- });
10
+ function checkRouteProduct($route, locationConfigParam) {
11
+ const product = getProductFromRoute($route);
14
12
 
15
13
  // alias for the homepage
16
- if (locationConfigParam === 'home' && name === 'home') {
14
+ if (locationConfigParam === 'home' && $route.name === 'home') {
17
15
  return true;
18
16
  } else if (locationConfigParam === product) {
19
17
  return true;
@@ -0,0 +1,14 @@
1
+ image: registry.suse.com/bci/bci-base:latest
2
+
3
+ stages:
4
+ - check_version
5
+ - build_catalog
6
+
7
+ variables:
8
+ REGISTRY: $CI_REGISTRY
9
+ REGISTRY_USER: $CI_REGISTRY_USER
10
+ REGISTRY_PASSWORD: $CI_REGISTRY_PASSWORD
11
+ IMAGE_NAMESPACE: $CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME
12
+
13
+ include:
14
+ - remote: 'https://raw.githubusercontent.com/rancher/dashboard/master/.gitlab/workflows/build-extension-catalog.gitlab-ci.yml'
package/creators/app/init CHANGED
@@ -34,6 +34,25 @@ if (args.length === 3) {
34
34
  fs.ensureDirSync(appFolder);
35
35
  }
36
36
 
37
+ let addGitlabWorkflow = false;
38
+
39
+ // Check for Gitlab integration option
40
+ if ( args.length > 3 ) {
41
+ for (let i = 3; i < args.length; i++) {
42
+ switch (args[i]) {
43
+ case '-l':
44
+ addGitlabWorkflow = true;
45
+ break;
46
+ default:
47
+ break;
48
+ }
49
+ }
50
+ }
51
+
52
+ if ( addGitlabWorkflow ) {
53
+ files.push('.gitlab-ci.yml');
54
+ }
55
+
37
56
  // Check that there is a package file
38
57
 
39
58
  let setName = false;
@@ -47,9 +47,18 @@ export default {
47
47
  selectedSeverityLabel: null,
48
48
  ignoredAnnotations: IGNORED_ANNOTATIONS,
49
49
  severityOptions: [
50
- this.t('prometheusRule.alertingRules.labels.severity.choices.critical'),
51
- this.t('prometheusRule.alertingRules.labels.severity.choices.warning'),
52
- this.t('prometheusRule.alertingRules.labels.severity.choices.none'),
50
+ {
51
+ label: this.t('prometheusRule.alertingRules.labels.severity.choices.critical'),
52
+ value: 'critical'
53
+ },
54
+ {
55
+ label: this.t('prometheusRule.alertingRules.labels.severity.choices.warning'),
56
+ value: 'warning'
57
+ },
58
+ {
59
+ label: this.t('prometheusRule.alertingRules.labels.severity.choices.none'),
60
+ value: 'none'
61
+ },
53
62
  ],
54
63
  };
55
64
  },
@@ -6,6 +6,7 @@ import { _VIEW } from '@shell/config/query-params';
6
6
  import ArrayListGrouped from '@shell/components/form/ArrayListGrouped';
7
7
  import AlertingRule from './AlertingRule';
8
8
  import RecordingRule from './RecordingRule';
9
+ import { clone } from '@shell/utils/object';
9
10
 
10
11
  export default {
11
12
  components: {
@@ -105,7 +106,7 @@ export default {
105
106
  });
106
107
  break;
107
108
  case 'alert':
108
- value.push(this.defaultAlert);
109
+ value.push(clone(this.defaultAlert));
109
110
  break;
110
111
  default:
111
112
  break;
@@ -1,6 +1,8 @@
1
1
  import { mount } from '@vue/test-utils';
2
2
  import CustomCommand from '@shell/edit/provisioning.cattle.io.cluster/CustomCommand.vue';
3
-
3
+ jest.mock('@shell/utils/clipboard', () => {
4
+ return { copyTextToClipboard: jest.fn(() => Promise.resolve({})) };
5
+ });
4
6
  describe('component: CustomCommand', () => {
5
7
  const token = 'MY_TOKEN';
6
8
  const ip = 'MY_IP';
@@ -40,12 +40,13 @@ export default {
40
40
  data() {
41
41
  const {
42
42
  strategy:strategyObj = {},
43
+ updateStrategy: updateStrategyObj = {},
43
44
  minReadySeconds = 0,
44
45
  progressDeadlineSeconds = 600,
45
46
  revisionHistoryLimit = 10,
46
47
  podManagementPolicy = 'OrderedReady'
47
48
  } = this.value;
48
- const strategy = strategyObj.type || 'RollingUpdate';
49
+ const strategy = strategyObj.type || updateStrategyObj.type || 'RollingUpdate';
49
50
  let maxSurge = '25';
50
51
  let maxUnavailable = '25';
51
52
  let surgeUnits = '%';
@@ -97,7 +98,7 @@ export default {
97
98
  case WORKLOAD_TYPES.DAEMON_SET:
98
99
  case WORKLOAD_TYPES.STATEFUL_SET:
99
100
  return {
100
- options: ['RollingUpdate', 'Delete'],
101
+ options: ['RollingUpdate', 'OnDelete'],
101
102
  labels: [this.t('workload.upgrading.strategies.labels.rollingUpdate'), this.t('workload.upgrading.strategies.labels.delete')]
102
103
  };
103
104
  default:
@@ -78,7 +78,8 @@ export default {
78
78
  * Required to initialize with default SC on creation
79
79
  */
80
80
  defaultStorageClassName() {
81
- return this.storageClasses.find((sc) => sc.metadata?.annotations?.['storageclass.beta.kubernetes.io/is-default-class'] || sc.metadata?.annotations?.['storageclass.kubernetes.io/is-default-class'])?.metadata.name;
81
+ return this.storageClasses.find((sc) => sc.metadata?.annotations?.['storageclass.beta.kubernetes.io/is-default-class'] === 'true' ||
82
+ sc.metadata?.annotations?.['storageclass.kubernetes.io/is-default-class'] === 'true')?.metadata.name ;
82
83
  },
83
84
 
84
85
  availablePVs() {
@@ -13,7 +13,7 @@ import { setContext, getLocation, getRouteData, normalizeError } from '../utils/
13
13
  import { createStore } from '../config/store.js';
14
14
 
15
15
  /* Plugins */
16
-
16
+ import { loadDirectives } from '@shell/plugins';
17
17
  import '../plugins/portal-vue.js';
18
18
  import cookieUniversalNuxt from '../utils/cookie-universal-nuxt.js';
19
19
  import axios from '../utils/axios.js';
@@ -21,11 +21,7 @@ import plugins from '../core/plugins.js';
21
21
  import pluginsLoader from '../core/plugins-loader.js';
22
22
  import axiosShell from '../plugins/axios';
23
23
  import '../plugins/tooltip';
24
- import '../plugins/clean-tooltip-directive';
25
- import '../plugins/vue-clipboard2';
26
24
  import '../plugins/v-select';
27
- import '../plugins/directives';
28
- import '../plugins/clean-html-directive';
29
25
  import '../plugins/transitions';
30
26
  import '../plugins/vue-js-modal';
31
27
  import '../plugins/js-yaml';
@@ -47,6 +43,29 @@ import '../plugins/formatters';
47
43
  import version from '../plugins/version';
48
44
  import steveCreateWorker from '../plugins/steve-create-worker';
49
45
 
46
+ // Prevent extensions from overriding existing directives
47
+ // Hook into Vue.directive and keep track of the directive names that have been added
48
+ // and prevent an existing directive from being overwritten
49
+ const directiveNames = {};
50
+ const vueDirective = Vue.directive;
51
+
52
+ Vue.directive = function(name) {
53
+ if (directiveNames[name]) {
54
+ console.log(`Can not override directive: ${ name }`); // eslint-disable-line no-console
55
+
56
+ return;
57
+ }
58
+
59
+ directiveNames[name] = true;
60
+
61
+ vueDirective.apply(Vue, arguments);
62
+ };
63
+
64
+ // Load the directives from the plugins - we do this with a function so we know
65
+ // these are initialized here, after the code above which keeps track of them and
66
+ // prevents over-writes
67
+ loadDirectives();
68
+
50
69
  // Component: <ClientOnly>
51
70
  Vue.component(ClientOnly.name, ClientOnly);
52
71
 
@@ -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.29",
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",
@@ -120,7 +120,7 @@
120
120
  "url-parse": "1.5.10",
121
121
  "v-tooltip": "2.0.3",
122
122
  "vue": "2.7.14",
123
- "vue-clipboard2": "0.3.1",
123
+ "clipboard-polyfill": "4.0.1",
124
124
  "vue-codemirror": "4.0.6",
125
125
  "vue-js-modal": "1.3.35",
126
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
+ };