@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.
- package/.DS_Store +0 -0
- package/assets/translations/en-us.yaml +16 -2
- package/assets/translations/zh-hans.yaml +1 -1
- package/chart/monitoring/grafana/index.vue +2 -2
- package/components/AsyncButton.vue +9 -0
- package/components/CopyCode.vue +6 -2
- package/components/CopyToClipboard.vue +2 -1
- package/components/CopyToClipboardText.vue +14 -9
- package/components/EtcdInfoBanner.vue +4 -4
- package/components/Markdown.vue +16 -12
- package/components/ResourceDetail/Masthead.vue +9 -6
- package/components/SortableTable/THead.vue +7 -9
- package/components/SortableTable/index.vue +1 -2
- package/components/StatusTable.vue +5 -1
- package/components/__tests__/CopyCode.test.ts +5 -4
- package/components/fleet/FleetBundles.vue +5 -11
- package/components/fleet/FleetStatus.vue +3 -3
- package/components/fleet/FleetSummary.vue +35 -30
- package/components/fleet/__tests__/FleetSummary.test.ts +316 -0
- package/components/form/Password.vue +3 -1
- package/components/nav/Header.vue +1 -1
- package/config/home-links.js +1 -1
- package/core/plugin-helpers.js +3 -5
- package/creators/app/files/.gitlab-ci.yml +14 -0
- package/creators/app/init +19 -0
- package/detail/provisioning.cattle.io.cluster.vue +2 -1
- package/edit/monitoring.coreos.com.prometheusrule/AlertingRule.vue +12 -3
- package/edit/monitoring.coreos.com.prometheusrule/GroupRules.vue +2 -1
- package/edit/provisioning.cattle.io.cluster/__tests__/CustomCommand.tests.ts +3 -1
- package/edit/workload/Upgrading.vue +3 -2
- package/edit/workload/index.vue +2 -1
- package/edit/workload/storage/persistentVolumeClaim/persistentvolumeclaim.vue +2 -1
- package/initialize/index.js +24 -5
- package/machine-config/__tests__/vmwarevsphere.test.ts +72 -0
- package/machine-config/vmwarevsphere.vue +68 -13
- package/models/__tests__/management.cattle.io.cluster.test.ts +4 -0
- package/models/management.cattle.io.cluster.js +7 -3
- package/models/provisioning.cattle.io.cluster.js +19 -1
- package/package.json +3 -2
- package/pages/c/_cluster/apps/charts/index.vue +64 -43
- package/plugins/clean-html-directive.js +1 -19
- package/plugins/clean-html.js +53 -0
- package/plugins/clean-tooltip-directive.js +1 -1
- package/plugins/index.js +11 -0
- package/rancher-components/BadgeState/BadgeState.vue +5 -1
- package/rancher-components/Banner/Banner.test.ts +51 -1
- package/rancher-components/Banner/Banner.vue +134 -53
- package/rancher-components/Card/Card.test.ts +37 -0
- package/rancher-components/Card/Card.vue +24 -7
- package/rancher-components/Form/Checkbox/Checkbox.test.ts +20 -29
- package/rancher-components/Form/Checkbox/Checkbox.vue +45 -20
- package/rancher-components/Form/LabeledInput/LabeledInput.test.ts +2 -8
- package/rancher-components/Form/LabeledInput/LabeledInput.vue +22 -10
- package/rancher-components/Form/Radio/RadioButton.test.ts +31 -0
- package/rancher-components/Form/Radio/RadioButton.vue +30 -13
- package/rancher-components/Form/Radio/RadioGroup.vue +26 -7
- package/rancher-components/Form/TextArea/TextAreaAutoGrow.vue +7 -6
- package/rancher-components/Form/ToggleSwitch/ToggleSwitch.test.ts +25 -38
- package/rancher-components/Form/ToggleSwitch/ToggleSwitch.vue +23 -11
- package/rancher-components/LabeledTooltip/LabeledTooltip.vue +19 -5
- package/rancher-components/StringList/StringList.test.ts +453 -49
- package/rancher-components/StringList/StringList.vue +92 -58
- package/scripts/.DS_Store +0 -0
- package/scripts/extension/bundle +19 -7
- package/scripts/extension/helm/scripts/package +11 -3
- package/scripts/extension/publish +20 -9
- package/scripts/verdaccio.log +205 -0
- package/store/index.js +3 -4
- package/types/shell/index.d.ts +15 -9
- package/utils/clipboard.js +5 -0
- package/yarn-error.log +200 -0
- package/plugins/vue-clipboard2.js +0 -4
|
@@ -39,7 +39,17 @@ const OS_OPTIONS = [
|
|
|
39
39
|
'linux',
|
|
40
40
|
'windows'
|
|
41
41
|
];
|
|
42
|
-
|
|
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',
|
|
222
|
-
set(this.value, 'diskSize',
|
|
223
|
-
set(this.value, 'memorySize',
|
|
224
|
-
set(this.value, 'hostsystem',
|
|
225
|
-
set(this.value, '
|
|
226
|
-
set(this.value, '
|
|
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',
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
411
|
+
try {
|
|
412
|
+
const config = await this.generateKubeConfig();
|
|
412
413
|
|
|
413
|
-
|
|
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
|
+
"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
|
-
"
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
180
|
-
|
|
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 =
|
|
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
|
-
|
|
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:
|
|
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.
|
|
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="
|
|
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="
|
|
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
|
-
|
|
545
|
+
.vs__search {
|
|
521
546
|
position: absolute;
|
|
522
547
|
right: 0
|
|
523
548
|
}
|
|
524
549
|
|
|
525
|
-
|
|
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
|
-
|
|
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
|
|
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
|
+
};
|
package/plugins/index.js
ADDED
|
@@ -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
|
|
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(
|
|
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
|
});
|