@rancher/shell 0.3.15 → 0.3.17

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 (155) hide show
  1. package/assets/images/wechat-qr-code.jpg +0 -0
  2. package/assets/translations/en-us.yaml +70 -15
  3. package/assets/translations/zh-hans.yaml +155 -33
  4. package/chart/__tests__/S3.test.ts +50 -0
  5. package/chart/rancher-backup/S3.vue +21 -0
  6. package/chart/rancher-backup/index.vue +4 -0
  7. package/cloud-credential/generic.vue +1 -1
  8. package/components/BannerGraphic.vue +1 -0
  9. package/components/CommunityLinks.vue +1 -0
  10. package/components/CruResource.vue +1 -1
  11. package/components/EmberPage.vue +1 -0
  12. package/components/FileDiff.vue +92 -85
  13. package/components/GrafanaDashboard.vue +7 -1
  14. package/components/ResourceDetail/index.vue +4 -12
  15. package/components/ResourceList/index.vue +1 -1
  16. package/components/ResourceTable.vue +50 -2
  17. package/components/SimpleBox.vue +1 -0
  18. package/components/SortableTable/index.vue +5 -1
  19. package/components/YamlEditor.vue +1 -0
  20. package/components/auth/RoleDetailEdit.vue +1 -0
  21. package/components/form/GitPicker.vue +1 -1
  22. package/components/form/NameNsDescription.vue +28 -12
  23. package/components/form/NodeAffinity.vue +2 -2
  24. package/components/form/PodAffinity.vue +8 -3
  25. package/components/form/ResourceTabs/index.vue +8 -2
  26. package/components/form/Select.vue +16 -0
  27. package/components/form/__tests__/NodeAffinity.test.ts +38 -0
  28. package/components/form/__tests__/PodAffinity.test.ts +46 -0
  29. package/components/formatter/ClusterLink.vue +8 -4
  30. package/components/formatter/ImageName.vue +23 -0
  31. package/components/formatter/PodImages.vue +7 -1
  32. package/components/formatter/__tests__/ClusterLink.test.ts +101 -0
  33. package/components/nav/Header.vue +2 -2
  34. package/config/__test__/home-links.test.ts +62 -0
  35. package/config/home-links.js +15 -3
  36. package/config/labels-annotations.js +5 -1
  37. package/config/product/auth.js +1 -1
  38. package/config/router.js +0 -9
  39. package/config/settings.ts +4 -0
  40. package/config/table-headers.js +6 -5
  41. package/config/uiplugins.js +50 -5
  42. package/core/plugin-helpers.js +20 -12
  43. package/core/plugin.ts +9 -0
  44. package/core/plugins.js +1 -1
  45. package/core/types-provisioning.ts +253 -0
  46. package/core/types.ts +17 -3
  47. package/detail/autoscaling.horizontalpodautoscaler/index.vue +50 -1
  48. package/detail/catalog.cattle.io.clusterrepo.vue +8 -1
  49. package/detail/node.vue +6 -6
  50. package/detail/pod.vue +2 -6
  51. package/detail/provisioning.cattle.io.cluster.vue +46 -7
  52. package/detail/workload/index.vue +9 -9
  53. package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +62 -0
  54. package/edit/__tests__/monitoring.coreos.com.prometheusrule.test.ts +56 -0
  55. package/edit/auth/github.vue +1 -0
  56. package/edit/autoscaling.horizontalpodautoscaler/hpa-scaling-rule.vue +130 -0
  57. package/edit/autoscaling.horizontalpodautoscaler/index.vue +79 -0
  58. package/edit/fleet.cattle.io.gitrepo.vue +18 -1
  59. package/edit/monitoring.coreos.com.prometheusrule/index.vue +8 -3
  60. package/edit/namespace.vue +9 -1
  61. package/edit/networking.k8s.io.ingress/RulePath.vue +0 -2
  62. package/edit/provisioning.cattle.io.cluster/AgentConfiguration.vue +1 -30
  63. package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +79 -1
  64. package/edit/provisioning.cattle.io.cluster/index.vue +52 -0
  65. package/edit/provisioning.cattle.io.cluster/rke2.vue +330 -150
  66. package/edit/ui.cattle.io.navlink.vue +2 -1
  67. package/initialize/App.js +3 -13
  68. package/initialize/layouts.ts +26 -0
  69. package/list/provisioning.cattle.io.cluster.vue +8 -1
  70. package/middleware/authenticated.js +93 -5
  71. package/mixins/brand.js +39 -3
  72. package/mixins/child-hook.js +2 -2
  73. package/mixins/create-edit-view/impl.js +2 -2
  74. package/models/fleet.cattle.io.gitrepo.js +1 -0
  75. package/models/provisioning.cattle.io.cluster.js +9 -1
  76. package/package.json +3 -3
  77. package/pages/about.vue +8 -2
  78. package/pages/auth/login.vue +10 -0
  79. package/pages/auth/logout.vue +11 -3
  80. package/pages/auth/setup.vue +4 -0
  81. package/pages/c/_cluster/apps/charts/index.vue +5 -2
  82. package/pages/c/_cluster/apps/charts/install.vue +5 -0
  83. package/pages/c/_cluster/auth/roles/index.vue +1 -1
  84. package/pages/c/_cluster/explorer/index.vue +1 -10
  85. package/pages/c/_cluster/uiplugins/AddExtensionRepos.vue +177 -0
  86. package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +19 -3
  87. package/pages/c/_cluster/uiplugins/RemoveUIPlugins.vue +90 -21
  88. package/pages/c/_cluster/uiplugins/SetupUIPlugins.vue +107 -37
  89. package/pages/c/_cluster/uiplugins/index.vue +155 -44
  90. package/pages/docs/_doc.vue +9 -3
  91. package/pages/home.vue +10 -5
  92. package/pages/support/index.vue +10 -4
  93. package/pkg/auto-import.js +1 -1
  94. package/plugins/clean-tooltip-directive.js +1 -1
  95. package/plugins/dashboard-store/resource-class.js +35 -2
  96. package/plugins/plugin.js +9 -1
  97. package/plugins/steve/actions.js +22 -0
  98. package/rancher-components/BadgeState/BadgeState.vue +5 -1
  99. package/rancher-components/Banner/Banner.test.ts +51 -1
  100. package/rancher-components/Banner/Banner.vue +134 -53
  101. package/rancher-components/Card/Card.vue +24 -7
  102. package/rancher-components/Form/Checkbox/Checkbox.test.ts +20 -29
  103. package/rancher-components/Form/Checkbox/Checkbox.vue +45 -20
  104. package/rancher-components/Form/LabeledInput/LabeledInput.test.ts +2 -8
  105. package/rancher-components/Form/LabeledInput/LabeledInput.vue +22 -10
  106. package/rancher-components/Form/Radio/RadioButton.vue +30 -13
  107. package/rancher-components/Form/Radio/RadioGroup.vue +26 -7
  108. package/rancher-components/Form/TextArea/TextAreaAutoGrow.vue +7 -6
  109. package/rancher-components/Form/ToggleSwitch/ToggleSwitch.test.ts +25 -38
  110. package/rancher-components/Form/ToggleSwitch/ToggleSwitch.vue +23 -11
  111. package/rancher-components/LabeledTooltip/LabeledTooltip.vue +19 -5
  112. package/rancher-components/StringList/StringList.test.ts +453 -49
  113. package/rancher-components/StringList/StringList.vue +92 -58
  114. package/scripts/extension/publish +2 -2
  115. package/scripts/typegen.sh +1 -0
  116. package/server/server-middleware.js +4 -12
  117. package/store/index.js +13 -0
  118. package/store/prefs.js +0 -3
  119. package/store/type-map.js +17 -29
  120. package/types/shell/index.d.ts +243 -90
  121. package/utils/kube.js +9 -0
  122. package/utils/object.js +27 -0
  123. package/utils/settings.ts +2 -2
  124. package/vue.config.js +3 -2
  125. package/pages/safeMode.vue +0 -17
  126. package/rancher-components/components/BadgeState/BadgeState.spec.ts +0 -12
  127. package/rancher-components/components/BadgeState/BadgeState.vue +0 -111
  128. package/rancher-components/components/BadgeState/index.ts +0 -1
  129. package/rancher-components/components/Banner/Banner.test.ts +0 -63
  130. package/rancher-components/components/Banner/Banner.vue +0 -244
  131. package/rancher-components/components/Banner/index.ts +0 -1
  132. package/rancher-components/components/Card/Card.vue +0 -167
  133. package/rancher-components/components/Card/index.ts +0 -1
  134. package/rancher-components/components/Form/Checkbox/Checkbox.test.ts +0 -68
  135. package/rancher-components/components/Form/Checkbox/Checkbox.vue +0 -420
  136. package/rancher-components/components/Form/Checkbox/index.ts +0 -1
  137. package/rancher-components/components/Form/LabeledInput/LabeledInput.test.ts +0 -23
  138. package/rancher-components/components/Form/LabeledInput/LabeledInput.vue +0 -355
  139. package/rancher-components/components/Form/LabeledInput/index.ts +0 -1
  140. package/rancher-components/components/Form/Radio/RadioButton.vue +0 -287
  141. package/rancher-components/components/Form/Radio/RadioGroup.vue +0 -254
  142. package/rancher-components/components/Form/Radio/index.ts +0 -2
  143. package/rancher-components/components/Form/TextArea/TextAreaAutoGrow.vue +0 -170
  144. package/rancher-components/components/Form/TextArea/index.ts +0 -1
  145. package/rancher-components/components/Form/ToggleSwitch/ToggleSwitch.test.ts +0 -94
  146. package/rancher-components/components/Form/ToggleSwitch/ToggleSwitch.vue +0 -149
  147. package/rancher-components/components/Form/ToggleSwitch/index.ts +0 -1
  148. package/rancher-components/components/Form/index.ts +0 -5
  149. package/rancher-components/components/LabeledTooltip/LabeledTooltip.vue +0 -151
  150. package/rancher-components/components/LabeledTooltip/index.ts +0 -1
  151. package/rancher-components/components/StringList/StringList.test.ts +0 -484
  152. package/rancher-components/components/StringList/StringList.vue +0 -611
  153. package/rancher-components/components/StringList/index.ts +0 -1
  154. /package/rancher-components/{components/Card → Card}/Card.test.ts +0 -0
  155. /package/rancher-components/{components/Form → Form}/Radio/RadioButton.test.ts +0 -0
package/plugins/plugin.js CHANGED
@@ -12,9 +12,17 @@ export default async function(context) {
12
12
  // Provide a mechanism to load the UI without the plugins loaded - in case there is a problem
13
13
  let loadPlugins = true;
14
14
 
15
- if (context.route?.path.endsWith('/safeMode')) {
15
+ const queryKeys = Object.keys(context.route?.query || {}).map((q) => q.toLowerCase());
16
+
17
+ if (queryKeys.includes('safemode')) {
16
18
  loadPlugins = false;
17
19
  console.warn('Safe Mode - plugins will not be loaded'); // eslint-disable-line no-console
20
+ setTimeout(() => {
21
+ context.store.dispatch('growl/success', {
22
+ title: context.store.getters['i18n/t']('plugins.safeMode.title'),
23
+ message: context.store.getters['i18n/t']('plugins.safeMode.message')
24
+ }, { root: true });
25
+ }, 1000);
18
26
  }
19
27
 
20
28
  if (loadPlugins) {
@@ -128,6 +128,10 @@ export default {
128
128
 
129
129
  finishDeferred(key, 'resolve', out);
130
130
 
131
+ if (opt.method === 'post' || opt.method === 'put') {
132
+ handleValidationWarnings(res);
133
+ }
134
+
131
135
  return out;
132
136
  });
133
137
  }
@@ -192,6 +196,24 @@ export default {
192
196
 
193
197
  return Promise.reject(out);
194
198
  }
199
+
200
+ function handleValidationWarnings(res) {
201
+ const warnings = (res.headers?.warning || '').split(',');
202
+
203
+ if (!warnings.length || !warnings[0]) {
204
+ return;
205
+ }
206
+
207
+ const message = warnings.reduce((message, warning) => {
208
+ return `${ message }\n${ warning.trim() }`;
209
+ }, `Validation Warnings for ${ opt.url }\n`);
210
+
211
+ if (process.env.dev) {
212
+ console.warn(`${ message }\n\n`, res.data); // eslint-disable-line no-console
213
+ } else {
214
+ console.debug(message); // eslint-disable-line no-console
215
+ }
216
+ }
195
217
  },
196
218
 
197
219
  promptMove({ commit, state }, resources) {
@@ -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
  });
@@ -27,6 +27,13 @@ export default Vue.extend({
27
27
  type: String,
28
28
  default: null
29
29
  },
30
+ /**
31
+ * Add icon for the banner
32
+ */
33
+ icon: {
34
+ type: String,
35
+ default: null
36
+ },
30
37
  /**
31
38
  * Toggles the banner's close button.
32
39
  */
@@ -58,31 +65,137 @@ export default Vue.extend({
58
65
  class="banner"
59
66
  :class="{
60
67
  [color]: true,
61
- closable,
62
- stacked
63
68
  }"
64
69
  >
65
- <slot>
66
- <t v-if="labelKey" :k="labelKey" :raw="true" />
67
- <span v-else-if="messageLabel">{{ messageLabel }}</span>
68
- <span v-else v-html="nlToBr(label)" />
69
- </slot>
70
- <div v-if="closable" class="closer" @click="$emit('close')">
71
- <i class="icon icon-2x icon-close closer-icon" />
70
+ <div
71
+ v-if="icon"
72
+ class="banner__icon"
73
+ data-testid="banner-icon"
74
+ >
75
+ <i
76
+ class="icon icon-2x"
77
+ :class="icon"
78
+ />
79
+ </div>
80
+ <div
81
+ class="banner__content"
82
+ data-testid="banner-content"
83
+ :class="{
84
+ closable,
85
+ stacked,
86
+ icon
87
+ }"
88
+ >
89
+ <slot>
90
+ <t
91
+ v-if="labelKey"
92
+ :k="labelKey"
93
+ :raw="true"
94
+ />
95
+ <span v-else-if="messageLabel">{{ messageLabel }}</span>
96
+ <span
97
+ v-else
98
+ v-clean-html="nlToBr(label)"
99
+ />
100
+ </slot>
101
+ <div
102
+ v-if="closable"
103
+ class="banner__content__closer"
104
+ @click="$emit('close')"
105
+ >
106
+ <i
107
+ data-testid="banner-close"
108
+ class="icon icon-close closer-icon"
109
+ />
110
+ </div>
72
111
  </div>
73
112
  </div>
74
113
  </template>
75
114
 
76
115
  <style lang="scss" scoped>
77
- $left-border-size: 4px;
116
+ $left-border-size: 4px;
117
+ $icon-size: 24px;
118
+
119
+ .banner {
120
+ display: flex;
121
+ margin: 15px 0;
122
+ position: relative;
123
+ width: 100%;
124
+ color: var(--body-text);
125
+
126
+ &__icon {
127
+ width: $icon-size * 2;
128
+ flex-grow: 1;
129
+ display: flex;
130
+ justify-content: center;
131
+ align-items: center;
132
+ box-sizing: content-box;
133
+
134
+ .primary & {
135
+ background: var(--primary);
136
+ }
137
+
138
+ .secondary & {
139
+ background: var(--default);
140
+ }
141
+
142
+ .success & {
143
+ background: var(--success);
144
+ }
78
145
 
79
- .banner {
146
+ .info & {
147
+ background: var(--info);
148
+ }
149
+
150
+ .warning & {
151
+ background: var(--warning);
152
+ }
153
+
154
+ .error & {
155
+ background: var(--error);
156
+ color: var(--primary-text);
157
+ }
158
+ }
159
+
160
+ &__content {
80
161
  padding: 10px;
81
- margin: 15px 0;
82
- width: 100%;
83
162
  transition: all 0.2s ease;
84
- position: relative;
85
163
  line-height: 20px;
164
+ width: 100%;
165
+ border-left: solid $left-border-size transparent;
166
+ display: flex;
167
+ gap: 3px;
168
+
169
+ .primary & {
170
+ background: var(--primary);
171
+ border-color: var(--primary);
172
+ }
173
+
174
+ .secondary & {
175
+ background: var(--default-banner-bg);
176
+ border-color: var(--default);
177
+ }
178
+
179
+ .success & {
180
+ background: var(--success-banner-bg);
181
+ border-color: var(--success);
182
+ }
183
+
184
+ .info & {
185
+ background: var(--info-banner-bg);
186
+ border-color: var(--info);
187
+ }
188
+
189
+ .warning & {
190
+ background: var(--warning-banner-bg);
191
+ border-color: var(--warning);
192
+ }
193
+
194
+ .error & {
195
+ background: var(--error-banner-bg);
196
+ border-color: var(--error);
197
+ color: var(--error);
198
+ }
86
199
 
87
200
  &.stacked {
88
201
  padding: 0 10px;
@@ -97,10 +210,10 @@ export default Vue.extend({
97
210
  }
98
211
 
99
212
  &.closable {
100
- padding-right: 40px;
213
+ padding-right: $icon-size * 2;
101
214
  }
102
215
 
103
- .closer {
216
+ &__closer {
104
217
  display: flex;
105
218
  align-items: center;
106
219
 
@@ -109,12 +222,11 @@ export default Vue.extend({
109
222
  top: 0;
110
223
  right: 0;
111
224
  bottom: 0;
112
- width: 40px;
113
- line-height: 42px;
225
+ width: $icon-size;
226
+ line-height: $icon-size;
114
227
  text-align: center;
115
228
 
116
229
  .closer-icon {
117
- font-size: 22px;
118
230
  opacity: 0.7;
119
231
 
120
232
  &:hover {
@@ -124,40 +236,9 @@ export default Vue.extend({
124
236
  }
125
237
  }
126
238
 
127
- &.primary {
128
- background: var(--primary);
129
- border-left: solid $left-border-size var(--primary);
130
- color: var(--body-text);
131
- }
132
-
133
- &.secondary {
134
- background: var(--default-banner-bg);
135
- border-left: solid $left-border-size var(--default);
136
- color: var(--body-text);
137
- }
138
-
139
- &.success {
140
- background: var(--success-banner-bg);
141
- border-left: solid $left-border-size var(--success);
142
- color: var(--body-text);
143
- }
144
-
145
- &.info {
146
- background: var(--info-banner-bg);
147
- border-left: solid $left-border-size var(--info);
148
- color: var(--body-text);
149
- }
150
-
151
- &.warning {
152
- background: var(--warning-banner-bg);
153
- border-left: solid $left-border-size var(--warning);
154
- color: var(--body-text);
155
- }
156
-
157
- &.error {
158
- background: var(--error-banner-bg);
159
- border-left: solid $left-border-size var(--error);
160
- color: var(--error);
239
+ &.icon {
240
+ border-left: none;
161
241
  }
162
242
  }
243
+ }
163
244
  </style>
@@ -49,28 +49,45 @@ export default Vue.extend({
49
49
  sticky: {
50
50
  type: Boolean,
51
51
  default: false,
52
- },
52
+ },
53
53
  }
54
54
  });
55
55
  </script>
56
56
 
57
57
  <template>
58
- <div class="card-container" :class="{'highlight-border': showHighlightBorder, 'card-sticky': sticky}">
58
+ <div
59
+ class="card-container"
60
+ :class="{'highlight-border': showHighlightBorder, 'card-sticky': sticky}"
61
+ data-testid="card"
62
+ >
59
63
  <div class="card-wrap">
60
- <div class="card-title">
64
+ <div
65
+ class="card-title"
66
+ data-testid="card-title-slot"
67
+ >
61
68
  <slot name="title">
62
69
  {{ title }}
63
70
  </slot>
64
71
  </div>
65
- <hr />
66
- <div class="card-body">
72
+ <hr>
73
+ <div
74
+ class="card-body"
75
+ data-testid="card-body-slot"
76
+ >
67
77
  <slot name="body">
68
78
  {{ content }}
69
79
  </slot>
70
80
  </div>
71
- <div v-if="showActions" class="card-actions">
81
+ <div
82
+ v-if="showActions"
83
+ class="card-actions"
84
+ data-testid="card-actions-slot"
85
+ >
72
86
  <slot name="actions">
73
- <button class="btn role-primary" @click="buttonAction">
87
+ <button
88
+ class="btn role-primary"
89
+ @click="buttonAction"
90
+ >
74
91
  {{ buttonText }}
75
92
  </button>
76
93
  </slot>
@@ -1,7 +1,13 @@
1
- import { shallowMount } from '@vue/test-utils';
1
+ import { shallowMount, Wrapper } from '@vue/test-utils';
2
2
  import { Checkbox } from './index';
3
3
 
4
- describe('Checkbox.vue', () => {
4
+ describe('checkbox.vue', () => {
5
+ const event = {
6
+ target: { tagName: 'input', href: null },
7
+ stopPropagation: () => { },
8
+ preventDefault: () => { }
9
+ } as unknown as MouseEvent;
10
+
5
11
  it('is unchecked by default', () => {
6
12
  const wrapper = shallowMount(Checkbox);
7
13
  const cbInput = wrapper.find('input[type="checkbox"]').element as HTMLInputElement;
@@ -16,7 +22,7 @@ describe('Checkbox.vue', () => {
16
22
  expect(cbInput.checked).toBe(true);
17
23
  });
18
24
 
19
- it('updates from false to true when props change', async () => {
25
+ it('updates from false to true when props change', async() => {
20
26
  const wrapper = shallowMount(Checkbox);
21
27
  const cbInput = wrapper.find('input[type="checkbox"]').element as HTMLInputElement;
22
28
 
@@ -27,51 +33,36 @@ describe('Checkbox.vue', () => {
27
33
  expect(cbInput.checked).toBe(true);
28
34
  });
29
35
 
30
- it('emits an input event with a true value', async () => {
31
- const wrapper = shallowMount(Checkbox);
32
- const event = {
33
- target: { tagName: 'input', href: null },
34
- stopPropagation: () => { },
35
- preventDefault: () => { }
36
- };
36
+ it('emits an input event with a true value', async() => {
37
+ const wrapper: Wrapper<InstanceType<typeof Checkbox>> = shallowMount(Checkbox);
37
38
 
38
- (wrapper.vm as any).clicked(event);
39
+ wrapper.vm.clicked(event);
39
40
  await wrapper.vm.$nextTick();
40
41
 
41
42
  expect(wrapper.emitted().input?.length).toBe(1);
42
43
  expect(wrapper.emitted().input?.[0][0]).toBe(true);
43
44
  });
44
45
 
45
- it('emits an input event with a custom valueWhenTrue', async () => {
46
+ it('emits an input event with a custom valueWhenTrue', async() => {
46
47
  const valueWhenTrue = 'BIG IF TRUE';
47
- const event = {
48
- target: { tagName: 'input', href: null },
49
- stopPropagation: () => { },
50
- preventDefault: () => { }
51
- };
52
48
 
53
- const wrapper = shallowMount(Checkbox, { propsData: { value: false, valueWhenTrue } });
49
+ const wrapper: Wrapper<InstanceType<typeof Checkbox>> = shallowMount(Checkbox, { propsData: { value: false, valueWhenTrue } });
54
50
 
55
- (wrapper.vm as any).clicked(event);
51
+ wrapper.vm.clicked(event);
56
52
  await wrapper.vm.$nextTick();
57
53
 
58
54
  expect(wrapper.emitted().input?.length).toBe(1);
59
55
  expect(wrapper.emitted().input?.[0][0]).toBe(valueWhenTrue);
60
56
  });
61
57
 
62
- it('updates from valueWhenTrue to falsy', async () => {
58
+ it('updates from valueWhenTrue to falsy', async() => {
63
59
  const valueWhenTrue = 'REAL HUGE IF FALSE';
64
- const event = {
65
- target: { tagName: 'input', href: null },
66
- stopPropagation: () => { },
67
- preventDefault: () => { }
68
- };
69
60
 
70
- const wrapper = shallowMount(Checkbox, { propsData: { value: valueWhenTrue, valueWhenTrue } });
61
+ const wrapper: Wrapper<InstanceType<typeof Checkbox>> = shallowMount(Checkbox, { propsData: { value: valueWhenTrue, valueWhenTrue } });
71
62
 
72
- (wrapper.vm as any).clicked(event);
63
+ wrapper.vm.clicked(event);
73
64
  await wrapper.vm.$nextTick();
74
65
 
75
- expect(wrapper.emitted().input?.[0][0]).toBe(null);
76
- })
66
+ expect(wrapper.emitted().input?.[0][0]).toBeNull();
67
+ });
77
68
  });
@@ -2,8 +2,11 @@
2
2
  import Vue, { PropType } from 'vue';
3
3
  import { _EDIT, _VIEW } from '@shell/config/query-params';
4
4
  import { addObject, removeObject } from '@shell/utils/array';
5
+ import cloneDeep from 'lodash/cloneDeep';
5
6
 
6
7
  export default Vue.extend({
8
+ name: 'Checkbox',
9
+
7
10
  props: {
8
11
  /**
9
12
  * The checkbox value.
@@ -46,8 +49,8 @@ export default Vue.extend({
46
49
  },
47
50
 
48
51
  /**
49
- * Display an indeterminate state. Useful for cases where a checkbox might
50
- * be the parent to child checkboxes, and we need to show that a subset of
52
+ * Display an indeterminate state. Useful for cases where a checkbox might
53
+ * be the parent to child checkboxes, and we need to show that a subset of
51
54
  * children are checked.
52
55
  */
53
56
  indeterminate: {
@@ -110,22 +113,22 @@ export default Vue.extend({
110
113
  primary: {
111
114
  type: Boolean,
112
115
  default: false
113
- },
116
+ },
114
117
  },
115
118
 
116
119
  computed: {
117
120
  /**
118
121
  * Determines if the checkbox is disabled.
119
- * @returns boolean: True when the disabled prop is true or when mode is
122
+ * @returns boolean: True when the disabled prop is true or when mode is
120
123
  * View.
121
124
  */
122
125
  isDisabled(): boolean {
123
126
  return (this.disabled || this.mode === _VIEW);
124
127
  },
125
128
  /**
126
- * Determines if the checkbox is checked when using custom values or
129
+ * Determines if the checkbox is checked when using custom values or
127
130
  * multiple values.
128
- * @returns boolean: True when at least one value is true in a collection or
131
+ * @returns boolean: True when at least one value is true in a collection or
129
132
  * when value matches `this.valueWhenTrue`.
130
133
  */
131
134
  isChecked(): boolean {
@@ -162,13 +165,15 @@ export default Vue.extend({
162
165
  const click = new CustomEvent('click', customEvent);
163
166
 
164
167
  // Flip the value
165
- if (this.isMulti(this.value)) {
168
+ const value = cloneDeep(this.value);
169
+
170
+ if (this.isMulti(value)) {
166
171
  if (this.isChecked) {
167
- removeObject(this.value, this.valueWhenTrue);
172
+ removeObject(value, this.valueWhenTrue);
168
173
  } else {
169
- addObject(this.value, this.valueWhenTrue);
174
+ addObject(value, this.valueWhenTrue);
170
175
  }
171
- this.$emit('input', this.value);
176
+ this.$emit('input', value);
172
177
  } else if (this.isString(this.valueWhenTrue)) {
173
178
  if (this.isChecked) {
174
179
  this.$emit('input', null);
@@ -176,7 +181,7 @@ export default Vue.extend({
176
181
  this.$emit('input', this.valueWhenTrue);
177
182
  }
178
183
  } else {
179
- this.$emit('input', !this.value);
184
+ this.$emit('input', !value);
180
185
  this.$el.dispatchEvent(click);
181
186
  }
182
187
  },
@@ -197,14 +202,17 @@ export default Vue.extend({
197
202
  * @param value A collection of values for the checkbox.
198
203
  */
199
204
  findTrueValues(value: boolean[]): boolean {
200
- return value.find(v => v === this.valueWhenTrue) || false;
205
+ return value.find((v) => v === this.valueWhenTrue) || false;
201
206
  }
202
207
  }
203
208
  });
204
209
  </script>
205
210
 
206
211
  <template>
207
- <div class="checkbox-outer-container" data-checkbox-ctrl>
212
+ <div
213
+ class="checkbox-outer-container"
214
+ data-checkbox-ctrl
215
+ >
208
216
  <label
209
217
  class="checkbox-container"
210
218
  :class="{ 'disabled': isDisabled}"
@@ -214,14 +222,13 @@ export default Vue.extend({
214
222
  @click="clicked($event)"
215
223
  >
216
224
  <input
217
- v-model="value"
218
225
  :checked="isChecked"
219
226
  :value="valueWhenTrue"
220
227
  type="checkbox"
221
228
  :tabindex="-1"
222
229
  :name="id"
223
230
  @click.stop.prevent
224
- />
231
+ >
225
232
  <span
226
233
  class="checkbox-custom"
227
234
  :class="{indeterminate: indeterminate}"
@@ -236,15 +243,33 @@ export default Vue.extend({
236
243
  :class="{ 'checkbox-primary': primary }"
237
244
  >
238
245
  <slot name="label">
239
- <t v-if="labelKey" :k="labelKey" :raw="true" />
246
+ <t
247
+ v-if="labelKey"
248
+ :k="labelKey"
249
+ :raw="true"
250
+ />
240
251
  <template v-else-if="label">{{ label }}</template>
241
- <i v-if="tooltipKey" v-tooltip="t(tooltipKey)" class="checkbox-info icon icon-info icon-lg" />
242
- <i v-else-if="tooltip" v-tooltip="tooltip" class="checkbox-info icon icon-info icon-lg" />
252
+ <i
253
+ v-if="tooltipKey"
254
+ v-clean-tooltip="t(tooltipKey)"
255
+ class="checkbox-info icon icon-info icon-lg"
256
+ />
257
+ <i
258
+ v-else-if="tooltip"
259
+ v-clean-tooltip="tooltip"
260
+ class="checkbox-info icon icon-info icon-lg"
261
+ />
243
262
  </slot>
244
263
  </span>
245
264
  </label>
246
- <div v-if="descriptionKey || description" class="checkbox-outer-container-description">
247
- <t v-if="descriptionKey" :k="descriptionKey" />
265
+ <div
266
+ v-if="descriptionKey || description"
267
+ class="checkbox-outer-container-description"
268
+ >
269
+ <t
270
+ v-if="descriptionKey"
271
+ :k="descriptionKey"
272
+ />
248
273
  <template v-else-if="description">
249
274
  {{ description }}
250
275
  </template>
@@ -6,15 +6,9 @@ describe('component: LabeledInput', () => {
6
6
  it('should emit input only once', () => {
7
7
  const value = '2';
8
8
  const delay = 1;
9
- const wrapper = mount(LabeledInput, {
9
+ const wrapper = mount(LabeledInput, {
10
10
  propsData: { delay },
11
- mocks: {
12
- $store: {
13
- getters: {
14
- 'i18n/t': jest.fn()
15
- }
16
- }
17
- }
11
+ mocks: { $store: { getters: { 'i18n/t': jest.fn() } } }
18
12
  });
19
13
 
20
14
  jest.useFakeTimers();