@rancher/shell 3.0.12-rc.4 → 3.0.12-rc.6

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 (159) hide show
  1. package/apis/intf/resources-api/resource-base.ts +42 -2
  2. package/apis/intf/resources-api/resource-instance.ts +101 -0
  3. package/apis/intf/resources-api/resources-api.ts +177 -23
  4. package/apis/intf/resources.ts +8 -6
  5. package/apis/resources/__tests__/resources-api-class.test.ts +309 -28
  6. package/apis/resources/resources-api-class.ts +232 -43
  7. package/assets/images/providers/traefik.png +0 -0
  8. package/assets/styles/global/_button.scss +1 -1
  9. package/assets/translations/en-us.yaml +49 -14
  10. package/chart/__tests__/rancher-monitoring-dashboards-index.test.ts +269 -0
  11. package/chart/rancher-monitoring-dashboards/index.vue +155 -0
  12. package/cloud-credential/__tests__/generic.test.ts +132 -0
  13. package/cloud-credential/generic.vue +33 -3
  14. package/components/ActionDropdownShell.vue +5 -3
  15. package/components/ButtonGroup.vue +26 -1
  16. package/components/CruResource.vue +51 -2
  17. package/components/EtcdInfoBanner.vue +2 -12
  18. package/components/GrafanaDashboard.vue +2 -8
  19. package/components/PromptRestore.vue +93 -32
  20. package/components/Questions/index.vue +1 -0
  21. package/components/Resource/Detail/Masthead/__tests__/index.test.ts +68 -14
  22. package/components/Resource/Detail/Masthead/index.vue +8 -3
  23. package/components/ResourceTable.vue +1 -0
  24. package/components/SortableTable/index.vue +4 -3
  25. package/components/Wizard.vue +14 -1
  26. package/components/__tests__/ButtonGroup.test.ts +56 -0
  27. package/components/__tests__/PromptRestore.test.ts +169 -19
  28. package/components/fleet/GitRepoAdvancedTab.vue +1 -0
  29. package/components/fleet/GitRepoMetadataTab.vue +5 -0
  30. package/components/fleet/HelmOpAppCoConfigTab.vue +4 -0
  31. package/components/fleet/HelmOpMetadataTab.vue +5 -0
  32. package/components/form/FileSelector.vue +39 -1
  33. package/components/form/PrivateRegistry.constants.ts +7 -0
  34. package/components/form/PrivateRegistry.vue +253 -18
  35. package/components/form/Security.vue +1 -1
  36. package/components/form/SelectOrCreateAuthSecret.vue +140 -17
  37. package/components/form/__tests__/FileSelector.test.ts +23 -0
  38. package/components/form/__tests__/PrivateRegistry.test.ts +463 -73
  39. package/components/form/__tests__/SelectOrCreateAuthSecret.test.ts +122 -0
  40. package/components/formatter/EtcdSnapshotName.vue +73 -0
  41. package/components/formatter/Translate.vue +22 -1
  42. package/components/nav/Group.vue +5 -10
  43. package/components/nav/Header.vue +8 -1
  44. package/components/nav/TopLevelMenu.vue +1 -30
  45. package/components/nav/__tests__/Group.test.ts +61 -0
  46. package/components/templates/default.vue +7 -0
  47. package/config/features.js +1 -0
  48. package/config/labels-annotations.js +3 -0
  49. package/config/product/manager.js +6 -0
  50. package/config/secret.ts +10 -0
  51. package/config/settings.ts +6 -2
  52. package/config/table-headers.js +5 -2
  53. package/config/types.js +7 -0
  54. package/core/__tests__/plugin-products-apply.test.ts +787 -0
  55. package/core/__tests__/plugin-products-extend.test.ts +189 -0
  56. package/core/__tests__/plugin-products-helpers.test.ts +81 -78
  57. package/core/__tests__/plugin-products-new.test.ts +389 -0
  58. package/core/__tests__/plugin-products-scenarios.test.ts +980 -0
  59. package/core/__tests__/plugin-products-side-menu.test.ts +1935 -0
  60. package/core/plugin-products-base.ts +201 -93
  61. package/core/plugin-products-extending.ts +1 -1
  62. package/core/plugin-products-external.ts +380 -0
  63. package/core/plugin-products-helpers.ts +28 -24
  64. package/core/plugin-products-internal.ts +207 -0
  65. package/core/plugin-products-top-level.ts +10 -5
  66. package/core/plugin-products-type-guards.ts +17 -6
  67. package/core/plugin-products.ts +5 -7
  68. package/core/plugin-types.ts +1 -389
  69. package/core/plugin.ts +10 -11
  70. package/core/types.ts +6 -202
  71. package/detail/__tests__/provisioning.cattle.io.cluster.test.ts +52 -0
  72. package/detail/pod.vue +1 -1
  73. package/detail/provisioning.cattle.io.cluster.vue +79 -5
  74. package/dialog/RotateEncryptionKeyDialog.vue +33 -9
  75. package/dialog/__tests__/RotateEncryptionKeyDialog.test.ts +78 -0
  76. package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +92 -0
  77. package/edit/__tests__/fleet.cattle.io.helmop.test.ts +101 -0
  78. package/edit/__tests__/management.cattle.io.setting.test.ts +2 -1
  79. package/edit/auth/AuthProviderWarningBanners.vue +12 -0
  80. package/edit/auth/__tests__/AuthProviderWarningBanners.test.ts +10 -1
  81. package/edit/auth/__tests__/azuread.test.ts +1 -0
  82. package/edit/auth/__tests__/github.test.ts +1 -0
  83. package/edit/auth/__tests__/oidc.test.ts +1 -0
  84. package/edit/auth/__tests__/saml.test.ts +1 -0
  85. package/edit/auth/ldap/__tests__/index.test.ts +1 -0
  86. package/edit/compliance.cattle.io.clusterscanprofile.vue +39 -41
  87. package/edit/fleet.cattle.io.gitrepo.vue +70 -16
  88. package/edit/fleet.cattle.io.helmop.vue +51 -5
  89. package/edit/helm.cattle.io.projecthelmchart.vue +1 -0
  90. package/edit/{management.cattle.io.setting.vue → management.cattle.io.setting/index.vue} +32 -9
  91. package/edit/management.cattle.io.setting/system-default-registry-pull-secrets.vue +81 -0
  92. package/edit/provisioning.cattle.io.cluster/SelectCredential.vue +3 -12
  93. package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +18 -0
  94. package/edit/provisioning.cattle.io.cluster/index.vue +1 -1
  95. package/edit/provisioning.cattle.io.cluster/rke2.vue +6 -2
  96. package/edit/provisioning.cattle.io.cluster/tabs/etcd/index.vue +0 -1
  97. package/edit/provisioning.cattle.io.cluster/tabs/networking/index.vue +2 -2
  98. package/edit/provisioning.cattle.io.cluster/tabs/registries/index.vue +14 -55
  99. package/list/management.cattle.io.user.vue +24 -5
  100. package/list/utils/management.cattle.io.cluster.utils.ts +1 -1
  101. package/machine-config/__tests__/generic.test.ts +163 -0
  102. package/machine-config/components/EC2Networking.vue +31 -7
  103. package/machine-config/components/__tests__/EC2Networking.test.ts +38 -15
  104. package/machine-config/generic.vue +32 -5
  105. package/mixins/resource-fetch.js +1 -1
  106. package/models/__tests__/provisioning.cattle.io.cluster.test.ts +156 -0
  107. package/models/__tests__/rke-machine.cattle.io.vmwarevspheremachinetemplate.test.ts +22 -0
  108. package/models/__tests__/secret.test.ts +68 -1
  109. package/models/management.cattle.io.cluster.js +21 -3
  110. package/models/pod.js +25 -12
  111. package/models/provisioning.cattle.io.cluster.js +59 -9
  112. package/models/rke-machine.cattle.io.vmwarevspheremachinetemplate.js +9 -0
  113. package/models/rke.cattle.io.etcdsnapshot.js +17 -9
  114. package/models/secret.js +19 -0
  115. package/models/workload.js +12 -7
  116. package/package.json +1 -1
  117. package/pages/auth/login.vue +1 -1
  118. package/pages/c/_cluster/apps/charts/__tests__/install.test.ts +569 -81
  119. package/pages/c/_cluster/apps/charts/install.vue +143 -28
  120. package/pages/c/_cluster/auth/config/index.vue +11 -4
  121. package/pages/c/_cluster/monitoring/index.vue +6 -1
  122. package/pkg/require-asset.lib.js +25 -0
  123. package/pkg/vue.config.js +7 -0
  124. package/plugins/dashboard-store/__tests__/resource-class.test.ts +84 -0
  125. package/plugins/dashboard-store/getters.js +0 -1
  126. package/plugins/dashboard-store/resource-class.js +62 -15
  127. package/plugins/steve/__tests__/steve-class-resource-api.test.ts +147 -0
  128. package/plugins/steve/steve-class.js +43 -0
  129. package/plugins/steve/subscribe.js +11 -0
  130. package/rancher-components/Form/LabeledInput/LabeledInput.test.ts +115 -0
  131. package/rancher-components/Form/LabeledInput/LabeledInput.vue +83 -3
  132. package/rancher-components/Form/TextArea/TextAreaAutoGrow.vue +30 -0
  133. package/rancher-components/Form/TextArea/__tests__/TextAreaAutoGrow.test.ts +95 -0
  134. package/rancher-components/RcButton/index.ts +1 -1
  135. package/rancher-components/RcDropdown/RcDropdownTrigger.vue +6 -1
  136. package/store/__tests__/auth.test.ts +18 -0
  137. package/store/__tests__/features.test.ts +131 -0
  138. package/store/__tests__/growl.test.ts +374 -0
  139. package/store/__tests__/modal.test.ts +131 -0
  140. package/store/__tests__/notifications.test.ts +466 -1
  141. package/store/__tests__/slideInPanel.test.ts +88 -0
  142. package/store/__tests__/type-map.utils.test.ts +433 -0
  143. package/store/__tests__/ui-context.test.ts +255 -0
  144. package/store/auth.js +11 -1
  145. package/store/features.js +5 -0
  146. package/store/plugins.js +6 -0
  147. package/types/rancher/steve.api.ts +2 -2
  148. package/types/shell/index.d.ts +120 -0
  149. package/types/store/dashboard-store.types.ts +2 -2
  150. package/types/store/type-map.ts +262 -1
  151. package/utils/__tests__/operation-cr.test.ts +34 -0
  152. package/utils/async.ts +2 -0
  153. package/utils/grafana.js +2 -17
  154. package/utils/monitoring.js +38 -1
  155. package/utils/operation-cr.js +19 -0
  156. package/utils/require-asset.ts +7 -0
  157. package/utils/validators/__tests__/private-registry.test.ts +27 -15
  158. package/utils/validators/private-registry.ts +15 -4
  159. package/core/__tests__/plugin-products.test.ts +0 -4694
@@ -127,6 +127,12 @@ export default {
127
127
  default: () => []
128
128
  },
129
129
 
130
+ // Used to be called before going to the next step in the wizard.
131
+ beforeNext: {
132
+ type: Function,
133
+ default: null
134
+ },
135
+
130
136
  stepsOptions: {
131
137
  type: Object,
132
138
  default: () => ({ editFirstStep: true })
@@ -200,6 +206,7 @@ export default {
200
206
  tocContainerHeight: 0,
201
207
  mainLayoutEl: null,
202
208
  throttledComputeTocContainerHeight: null,
209
+ nextValidating: false,
203
210
  /**
204
211
  * Initialised on demand (given that it needs to make a request to fetch schema definition)
205
212
  */
@@ -488,6 +495,47 @@ export default {
488
495
  return slot !== 'default' && typeof this.$slots[slot] === 'function';
489
496
  },
490
497
 
498
+ async wizardBeforeGoToStep(fromStep, toStep) {
499
+ if (!this.beforeNext) {
500
+ return;
501
+ }
502
+
503
+ const fromIdx = this.steps.findIndex((s) => s.name === fromStep.name);
504
+ const toIdx = this.steps.findIndex((s) => s.name === toStep.name);
505
+
506
+ if (toIdx <= fromIdx) {
507
+ return;
508
+ }
509
+
510
+ try {
511
+ await this.beforeNext(fromStep);
512
+ this.$emit('error', []);
513
+ } catch (err) {
514
+ this.$emit('error', exceptionToErrorsArray(err));
515
+ throw err;
516
+ }
517
+ },
518
+
519
+ async handleNext(nextFn, activeStep) {
520
+ if (!this.beforeNext) {
521
+ nextFn();
522
+
523
+ return;
524
+ }
525
+
526
+ this.nextValidating = true;
527
+
528
+ try {
529
+ await this.beforeNext(activeStep);
530
+ this.$emit('error', []);
531
+ nextFn();
532
+ } catch (err) {
533
+ this.$emit('error', exceptionToErrorsArray(err));
534
+ } finally {
535
+ this.nextValidating = false;
536
+ }
537
+ },
538
+
491
539
  formatError(err) {
492
540
  if ( typeof err === 'string') {
493
541
  return err;
@@ -751,6 +799,7 @@ export default {
751
799
  :edit-first-step="stepsOptions.editFirstStep"
752
800
  :errors="errors"
753
801
  :finish-mode="finishMode"
802
+ :before-go-to-step="wizardBeforeGoToStep"
754
803
  class="wizard"
755
804
  @error="e=>errors = e"
756
805
  >
@@ -834,10 +883,10 @@ export default {
834
883
  name="next"
835
884
  >
836
885
  <button
837
- :disabled="!canNext"
886
+ :disabled="!canNext || nextValidating"
838
887
  type="button"
839
888
  class="btn role-primary"
840
- @click="next()"
889
+ @click="handleNext(next, activeStep)"
841
890
  >
842
891
  <t k="wizard.next" />
843
892
  </button>
@@ -3,23 +3,13 @@ import { Banner } from '@components/Banner';
3
3
  import Loading from '@shell/components/Loading';
4
4
  import { mapGetters } from 'vuex';
5
5
  import { hasLeader, leaderChanges, failedProposals } from '@shell/utils/grafana';
6
- import { CATALOG } from '@shell/config/types';
6
+ import { fetchMonitoringVersion } from '@shell/utils/monitoring';
7
7
 
8
8
  export default {
9
9
  components: { Banner, Loading },
10
10
  async fetch() {
11
11
  const inStore = this.$store.getters['currentProduct'].inStore;
12
- let monitoringVersion = '';
13
-
14
- if (this.$store.getters[`${ inStore }/canList`](CATALOG.APP)) {
15
- try {
16
- const res = await this.$store.dispatch(`${ inStore }/find`, { type: CATALOG.APP, id: 'cattle-monitoring-system/rancher-monitoring' });
17
-
18
- monitoringVersion = res?.currentVersion;
19
- } catch (err) {
20
-
21
- }
22
- }
12
+ const monitoringVersion = await fetchMonitoringVersion(this.$store, inStore);
23
13
 
24
14
  const leader = await hasLeader(monitoringVersion, this.$store.dispatch, this.currentCluster.id);
25
15
 
@@ -2,7 +2,7 @@
2
2
  import Loading from '@shell/components/Loading';
3
3
  import { Banner } from '@components/Banner';
4
4
  import { computeDashboardUrl } from '@shell/utils/grafana';
5
- import { CATALOG } from '@shell/config/types';
5
+ import { fetchMonitoringVersion } from '@shell/utils/monitoring';
6
6
 
7
7
  export default {
8
8
  components: { Banner, Loading },
@@ -41,13 +41,7 @@ export default {
41
41
  async fetch() {
42
42
  const inStore = this.$store.getters['currentProduct'].inStore;
43
43
 
44
- if (this.$store.getters[`${ inStore }/canList`](CATALOG.APP)) {
45
- try {
46
- const res = await this.$store.dispatch(`${ inStore }/find`, { type: CATALOG.APP, id: 'cattle-monitoring-system/rancher-monitoring' });
47
-
48
- this.monitoringVersion = res?.currentVersion;
49
- } catch (err) {}
50
- }
44
+ this.monitoringVersion = await fetchMonitoringVersion(this.$store, inStore);
51
45
  },
52
46
  data() {
53
47
  return {
@@ -7,16 +7,15 @@ import Date from '@shell/components/formatter/Date.vue';
7
7
  import RadioGroup from '@components/Form/Radio/RadioGroup.vue';
8
8
  import LabeledSelect from '@shell/components/form/LabeledSelect.vue';
9
9
  import { exceptionToErrorsArray } from '@shell/utils/error';
10
- import { CAPI, SNAPSHOT } from '@shell/config/types';
10
+ import { CAPI, SNAPSHOT, OPERATION, MANAGEMENT } from '@shell/config/types';
11
11
  import { set } from '@shell/utils/object';
12
12
  import ChildHook, { BEFORE_SAVE_HOOKS } from '@shell/mixins/child-hook';
13
13
  import { DATE_FORMAT, TIME_FORMAT } from '@shell/store/prefs';
14
14
  import { escapeHtml } from '@shell/utils/string';
15
15
  import day from 'dayjs';
16
16
  import { sortBy } from '@shell/utils/sort';
17
- import { STATES_ENUM } from '@shell/plugins/dashboard-store/resource-class';
18
17
  import AppModal from '@shell/components/AppModal.vue';
19
-
18
+ import { createOperationCR } from '@shell/utils/operation-cr';
20
19
  export default {
21
20
  components: {
22
21
  Card,
@@ -66,8 +65,41 @@ export default {
66
65
  return !!this.snapshot;
67
66
  },
68
67
 
69
- isRke2() {
70
- return !!this.snapshot?.rke2;
68
+ targetCluster() {
69
+ if (this.isCluster) {
70
+ return this.toRestore?.[0] || null;
71
+ }
72
+
73
+ if (this.snapshot?.cluster) {
74
+ return this.snapshot.cluster;
75
+ }
76
+
77
+ const snapshotClusterName = this.snapshot?.spec?.clusterName || this.snapshot?.clusterName;
78
+ const snapshotClusterId = this.snapshot?.clusterId || (snapshotClusterName ? `${ this.snapshot?.metadata?.namespace }/${ snapshotClusterName }` : null);
79
+
80
+ if (!snapshotClusterId) {
81
+ return null;
82
+ }
83
+
84
+ return this.$store.getters['management/byId'](CAPI.RANCHER_CLUSTER, snapshotClusterId);
85
+ },
86
+
87
+ targetMgmtCluster() {
88
+ if (this.targetCluster?.mgmt) {
89
+ return this.targetCluster.mgmt;
90
+ }
91
+
92
+ const mgmtClusterId = this.snapshot?.spec?.clusterName;
93
+
94
+ if (!mgmtClusterId) {
95
+ return null;
96
+ }
97
+
98
+ return this.$store.getters['management/byId'](MANAGEMENT.CLUSTER, mgmtClusterId);
99
+ },
100
+
101
+ isImported() {
102
+ return !!(this.targetCluster?.isImported || this.targetMgmtCluster?.isImported);
71
103
  },
72
104
 
73
105
  clusterSnapshots() {
@@ -77,6 +109,13 @@ export default {
77
109
  return [];
78
110
  }
79
111
  },
112
+ restoreModeLabels() {
113
+ return [
114
+ this.t('promptRestore.restoreMode.onlyEtcd'),
115
+ this.t('promptRestore.restoreMode.kubernetesVersionAndEtcd'),
116
+ this.t('promptRestore.restoreMode.clusterConfigKubernetesVersionAndEtcd')
117
+ ];
118
+ },
80
119
  restoreModeOptions() {
81
120
  return ['none', 'kubernetesVersion', 'all'];
82
121
  }
@@ -112,7 +151,7 @@ export default {
112
151
  const promise = this.$store.dispatch('management/findAll', { type: SNAPSHOT }).then((snapshots) => {
113
152
  const toRestoreClusterName = cluster?.clusterName || cluster?.metadata?.name;
114
153
 
115
- return snapshots.filter((s) => s?.snapshotFile?.status === STATES_ENUM.SUCCESSFUL && s.clusterName === toRestoreClusterName
154
+ return snapshots.filter((s) => s?.restoreEnabled && s.clusterName === toRestoreClusterName
116
155
  );
117
156
  });
118
157
 
@@ -143,19 +182,41 @@ export default {
143
182
 
144
183
  async apply(buttonDone) {
145
184
  try {
146
- const cluster = this.$store.getters['management/byId'](CAPI.RANCHER_CLUSTER, this.snapshot.clusterId);
185
+ const cluster = this.targetCluster;
186
+ const mgmtCluster = cluster?.mgmt || this.targetMgmtCluster;
147
187
 
148
- await this.applyHooks(BEFORE_SAVE_HOOKS);
149
-
150
- const now = cluster.spec?.rkeConfig?.etcdSnapshotRestore?.generation || 0;
188
+ const isImportedWithDayTwoOps = cluster?.isImportedWithDayTwoOps || mgmtCluster?.isDayTwoOpsEnabled;
151
189
 
152
- set(cluster, 'spec.rkeConfig.etcdSnapshotRestore', {
153
- generation: now + 1,
154
- name: this.snapshot.name,
155
- restoreRKEConfig: this.restoreMode,
156
- });
190
+ await this.applyHooks(BEFORE_SAVE_HOOKS);
157
191
 
158
- await cluster.save();
192
+ // For imported clusters with day 2 ops enabled, create an operation CR
193
+ if (isImportedWithDayTwoOps) {
194
+ if (!mgmtCluster) {
195
+ throw new Error(this.t('promptRestore.error.unableToResolveTargetCluster'));
196
+ }
197
+ const namespace = mgmtCluster?.id;
198
+ const safePrefix = mgmtCluster?.id;
199
+ const spec = {
200
+ clusterRef: {
201
+ apiVersion: 'management.cattle.io/v3',
202
+ kind: 'Cluster',
203
+ name: mgmtCluster?.id,
204
+ },
205
+ args: { name: this.snapshot?.snapshotFile?.name },
206
+ };
207
+
208
+ createOperationCR(this.$store.dispatch, OPERATION.ETCD_SNAPSHOT_RESTORE, spec, namespace, safePrefix);
209
+ } else {
210
+ const now = cluster.spec?.rkeConfig?.etcdSnapshotRestore?.generation || 0;
211
+
212
+ set(cluster, 'spec.rkeConfig.etcdSnapshotRestore', {
213
+ generation: now + 1,
214
+ name: this.snapshot.name,
215
+ restoreRKEConfig: this.restoreMode,
216
+ });
217
+
218
+ await cluster.save();
219
+ }
159
220
 
160
221
  this.$store.dispatch('growl/success', {
161
222
  title: this.t('promptRestore.notification.title'),
@@ -233,18 +294,25 @@ export default {
233
294
  />
234
295
  </p>
235
296
  </div>
236
- <div class="spacer" />
237
- <RadioGroup
238
- v-model:value="restoreMode"
239
- name="restoreMode"
240
- label="Restore Type"
241
- :labels="['Only etcd', 'Kubernetes version and etcd', 'Cluster config, Kubernetes version and etcd']"
242
- :options="restoreModeOptions"
243
- />
297
+ <div v-if="!isImported">
298
+ <div class="spacer" />
299
+ <RadioGroup
300
+ v-model:value="restoreMode"
301
+ name="restoreMode"
302
+ :label="t('promptRestore.restoreMode.label')"
303
+ :labels="restoreModeLabels"
304
+ :options="restoreModeOptions"
305
+ />
306
+ </div>
244
307
  </form>
245
308
  </div>
246
309
  </template>
247
-
310
+ <Banner
311
+ v-for="(err, i) in errors"
312
+ :key="i"
313
+ color="error"
314
+ :label="err"
315
+ />
248
316
  <template #actions>
249
317
  <div class="dialog-actions">
250
318
  <button
@@ -259,13 +327,6 @@ export default {
259
327
  :disabled="!hasSnapshot"
260
328
  @click="apply"
261
329
  />
262
-
263
- <Banner
264
- v-for="(err, i) in errors"
265
- :key="i"
266
- color="error"
267
- :label="err"
268
- />
269
330
  </div>
270
331
  </template>
271
332
  </Card>
@@ -474,6 +474,7 @@ export default {
474
474
  :value="get(value, q.variable)"
475
475
  :disabled="disabled"
476
476
  :chart-name="chartName"
477
+ :mode="mode"
477
478
  @update:value="update(q.variable, $event)"
478
479
  />
479
480
  </div>
@@ -1,5 +1,7 @@
1
1
  import { mount } from '@vue/test-utils';
2
2
  import Masthead from '@shell/components/Resource/Detail/Masthead/index.vue';
3
+ import TitleBar from '@shell/components/Resource/Detail/TitleBar/index.vue';
4
+ import Metadata from '@shell/components/Resource/Detail/Metadata/index.vue';
3
5
  import Cards from '@shell/components/Resource/Detail/Cards.vue';
4
6
 
5
7
  jest.mock('@shell/utils/clipboard', () => ({ copyTextToClipboard: jest.fn() }));
@@ -19,16 +21,18 @@ describe('component: Masthead/index', () => {
19
21
  metadataProps: { items: [] }
20
22
  };
21
23
 
24
+ const globalStubs = {
25
+ stubs: {
26
+ TitleBar: true,
27
+ Metadata: true,
28
+ Cards: true
29
+ }
30
+ };
31
+
22
32
  it('should render the Cards component', () => {
23
33
  const wrapper = mount(Masthead, {
24
34
  props: defaultProps,
25
- global: {
26
- stubs: {
27
- TitleBar: true,
28
- Metadata: true,
29
- Cards: true
30
- }
31
- }
35
+ global: globalStubs
32
36
  });
33
37
 
34
38
  expect(wrapper.findComponent(Cards).exists()).toBe(true);
@@ -37,17 +41,67 @@ describe('component: Masthead/index', () => {
37
41
  it('should pass the resource from titleBarProps to Cards', () => {
38
42
  const wrapper = mount(Masthead, {
39
43
  props: defaultProps,
40
- global: {
41
- stubs: {
42
- TitleBar: true,
43
- Metadata: true,
44
- Cards: true
45
- }
46
- }
44
+ global: globalStubs
47
45
  });
48
46
 
49
47
  const cardsComponent = wrapper.findComponent(Cards);
50
48
 
51
49
  expect(cardsComponent.props('resource')).toStrictEqual(mockResource);
52
50
  });
51
+
52
+ it('should render TitleBar when titleBarProps is provided', () => {
53
+ const wrapper = mount(Masthead, {
54
+ props: defaultProps,
55
+ global: globalStubs
56
+ });
57
+
58
+ expect(wrapper.findComponent(TitleBar).exists()).toBe(true);
59
+ });
60
+
61
+ it('should render Metadata when metadataProps is provided', () => {
62
+ const wrapper = mount(Masthead, {
63
+ props: defaultProps,
64
+ global: globalStubs
65
+ });
66
+
67
+ expect(wrapper.findComponent(Metadata).exists()).toBe(true);
68
+ });
69
+
70
+ it('should not render TitleBar when titleBarProps is undefined', () => {
71
+ const wrapper = mount(Masthead, {
72
+ props: { titleBarProps: undefined, metadataProps: { items: [] } },
73
+ global: globalStubs
74
+ });
75
+
76
+ expect(wrapper.findComponent(TitleBar).exists()).toBe(false);
77
+ });
78
+
79
+ it('should not render Metadata when metadataProps is undefined', () => {
80
+ const wrapper = mount(Masthead, {
81
+ props: { titleBarProps: defaultProps.titleBarProps, metadataProps: undefined },
82
+ global: globalStubs
83
+ });
84
+
85
+ expect(wrapper.findComponent(Metadata).exists()).toBe(false);
86
+ });
87
+
88
+ it('should not render Cards when titleBarProps is undefined', () => {
89
+ const wrapper = mount(Masthead, {
90
+ props: { titleBarProps: undefined, metadataProps: undefined },
91
+ global: globalStubs
92
+ });
93
+
94
+ expect(wrapper.findComponent(Cards).exists()).toBe(false);
95
+ });
96
+
97
+ it('should not render any children when both props are undefined', () => {
98
+ const wrapper = mount(Masthead, {
99
+ props: { titleBarProps: undefined, metadataProps: undefined },
100
+ global: globalStubs
101
+ });
102
+
103
+ expect(wrapper.findComponent(TitleBar).exists()).toBe(false);
104
+ expect(wrapper.findComponent(Metadata).exists()).toBe(false);
105
+ expect(wrapper.findComponent(Cards).exists()).toBe(false);
106
+ });
53
107
  });
@@ -4,8 +4,8 @@ import Metadata, { MetadataProps } from '@shell/components/Resource/Detail/Metad
4
4
  import Cards from '@shell/components/Resource/Detail/Cards.vue';
5
5
 
6
6
  export interface MastheadProps {
7
- titleBarProps: TitleBarProps;
8
- metadataProps: MetadataProps;
7
+ titleBarProps?: TitleBarProps;
8
+ metadataProps?: MetadataProps;
9
9
  }
10
10
  </script>
11
11
 
@@ -15,7 +15,10 @@ const props = defineProps<MastheadProps>();
15
15
  </script>
16
16
  <template>
17
17
  <div class="masthead">
18
- <TitleBar v-bind="props.titleBarProps">
18
+ <TitleBar
19
+ v-if="props.titleBarProps"
20
+ v-bind="props.titleBarProps"
21
+ >
19
22
  <template
20
23
  v-if="$slots['additional-actions']"
21
24
  #additional-actions
@@ -24,10 +27,12 @@ const props = defineProps<MastheadProps>();
24
27
  </template>
25
28
  </TitleBar>
26
29
  <Metadata
30
+ v-if="props.metadataProps"
27
31
  class="metadata-section"
28
32
  v-bind="props.metadataProps"
29
33
  />
30
34
  <Cards
35
+ v-if="props.titleBarProps"
31
36
  class="cards-section"
32
37
  :resource="props.titleBarProps.resource"
33
38
  />
@@ -746,6 +746,7 @@ export default {
746
746
  <ButtonGroup
747
747
  v-model:value="group"
748
748
  :options="_groupOptions"
749
+ size="medium"
749
750
  />
750
751
  </template>
751
752
 
@@ -1125,7 +1125,6 @@ export default {
1125
1125
  v-clean-tooltip="actionTooltip"
1126
1126
  type="button"
1127
1127
  variant="primary"
1128
- size="large"
1129
1128
  :class="{[bulkActionClass]:true}"
1130
1129
  :disabled="!act.enabled"
1131
1130
  :data-testid="componentTestid + '-' + act.action"
@@ -1146,6 +1145,7 @@ export default {
1146
1145
  :disabled="!selectedRows.length"
1147
1146
  :hidden-actions="hiddenActions"
1148
1147
  :action-tooltip="actionTooltip"
1148
+ size="medium"
1149
1149
  @click="applyTableAction"
1150
1150
  @mouseover="setBulkActionOfInterest"
1151
1151
  @mouseleave="setBulkActionOfInterest"
@@ -1158,10 +1158,11 @@ export default {
1158
1158
  :disable-button="!selectedRows.length"
1159
1159
  size="sm"
1160
1160
  >
1161
- <template #button-content>
1161
+ <template #button-content="{ buttonSize }">
1162
1162
  <button
1163
1163
  ref="actionDropDown"
1164
1164
  class="btn bg-primary mr-0"
1165
+ :class="buttonSize"
1165
1166
  :disabled="!selectedRows.length"
1166
1167
  >
1167
1168
  <i class="icon icon-gear" />
@@ -1874,7 +1875,7 @@ export default {
1874
1875
  }
1875
1876
 
1876
1877
  .search-box {
1877
- height: 40px;
1878
+ height: 32px;
1878
1879
  margin-left: 10px;
1879
1880
  min-width: 180px;
1880
1881
  }
@@ -124,6 +124,11 @@ export default {
124
124
  errors: {
125
125
  type: Array,
126
126
  default: null,
127
+ },
128
+
129
+ beforeGoToStep: {
130
+ type: Function,
131
+ default: null
127
132
  }
128
133
  },
129
134
 
@@ -217,7 +222,7 @@ export default {
217
222
  },
218
223
 
219
224
  methods: {
220
- goToStep(number, fromNav) {
225
+ async goToStep(number, fromNav) {
221
226
  if (number < 1) {
222
227
  return;
223
228
  }
@@ -233,6 +238,14 @@ export default {
233
238
  return;
234
239
  }
235
240
 
241
+ if (this.beforeGoToStep && fromNav) {
242
+ try {
243
+ await this.beforeGoToStep(this.activeStep, selected);
244
+ } catch {
245
+ return;
246
+ }
247
+ }
248
+
236
249
  this.activeStep = selected;
237
250
 
238
251
  this.$emit('next', { step: selected });
@@ -118,4 +118,60 @@ describe('component: ButtonGroup', () => {
118
118
  expect(wrapper.emitted('update:value')).toHaveLength(1);
119
119
  expect(wrapper.emitted('update:value')![0][0]).toBe('val1');
120
120
  });
121
+
122
+ it.each([
123
+ ['small', 'btn-sm'],
124
+ ['medium', 'btn-md'],
125
+ ])('should apply the size class to each button when size is %s', (size, sizeClass) => {
126
+ const options = [
127
+ {
128
+ label: 'label1',
129
+ value: 'val1'
130
+ },
131
+ {
132
+ label: 'label2',
133
+ value: 'val2'
134
+ },
135
+ ];
136
+
137
+ const wrapper = shallowMount(ButtonGroup, {
138
+ props: {
139
+ options,
140
+ value: 'val1',
141
+ size
142
+ }
143
+ });
144
+
145
+ const buttons = wrapper.findAll('button');
146
+
147
+ expect(buttons).toHaveLength(2);
148
+ buttons.forEach((button) => {
149
+ expect(button.classes()).toContain(sizeClass);
150
+ });
151
+ });
152
+
153
+ it.each([
154
+ [undefined],
155
+ ['large'],
156
+ ])('should not apply a size class when size is %s', (size) => {
157
+ const options = [
158
+ {
159
+ label: 'label1',
160
+ value: 'val1'
161
+ },
162
+ ];
163
+
164
+ const wrapper = shallowMount(ButtonGroup, {
165
+ props: {
166
+ options,
167
+ value: 'val1',
168
+ ...(size ? { size } : {})
169
+ }
170
+ });
171
+
172
+ const button = wrapper.find('button');
173
+
174
+ expect(button.classes()).not.toContain('btn-sm');
175
+ expect(button.classes()).not.toContain('btn-md');
176
+ });
121
177
  });