@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
package/core/types.ts CHANGED
@@ -1,13 +1,9 @@
1
- import { ProductFunction } from './plugin';
2
1
  import { RouteRecordRaw } from 'vue-router';
3
2
  import type { ExtensionManager } from '@shell/types/extension-manager';
4
3
  import { PaginationSettingsStores } from '@shell/types/resources/settings';
5
- import type {
6
- ProductMetadata, ProductSinglePage,
7
- StandardProductName, RouteRecordRawWithParams, ProductChild,
8
- ProductChildGroup,
9
- ProductChildPage
10
- } from './plugin-types';
4
+ import { IExtensionProducts } from '@shell/core/plugin-products-external';
5
+ import { RouteRecordRawWithParams } from '@shell/core/plugin-types';
6
+ import { TypeMapProduct } from '@shell/types/store/type-map';
11
7
 
12
8
  // Cluster Provisioning types
13
9
  export * from './types-provisioning';
@@ -200,137 +196,6 @@ export type ExtensionEnvironment = {
200
196
  docsVersion: string; /** e.g. 'v2.10' */
201
197
  };
202
198
 
203
- export interface ProductOptions {
204
- /**
205
- * The category this product belongs under. i.e. 'config'
206
- */
207
- category?: string;
208
-
209
- /**
210
- * Hide the Copy KubeConfig button in the header
211
- */
212
- hideCopyConfig?: boolean;
213
-
214
- /**
215
- * Hide the Download KubeConfig button in the header
216
- */
217
- hideKubeConfig?: boolean;
218
-
219
- /**
220
- * Hide the Kubectl Shell button in the header
221
- */
222
- hideKubeShell?: boolean;
223
-
224
- /**
225
- * Hide the Namespace location
226
- */
227
- hideNamespaceLocation?: boolean;
228
-
229
- /**
230
- * Hide the system resources
231
- */
232
-
233
- hideSystemResources?: boolean;
234
- /**
235
- * The icon that should be displayed beside this item in the navigation.
236
- */
237
- icon?: string,
238
-
239
- /**
240
- * Only load the product if the feature is present
241
- */
242
- ifFeature?: string | RegExp;
243
-
244
- /**
245
- * Only load the product if the type is present
246
- */
247
- ifHave?: string;
248
-
249
- /**
250
- * Only load the product if the group is present
251
- */
252
- ifHaveGroup?: string | RegExp;
253
-
254
- /**
255
- * Only load the product if the type is present
256
- */
257
- ifHaveType?: string | RegExp;
258
-
259
- /**
260
- * Hide the product if the type is present (opposite of ifHaveType)
261
- */
262
- ifNotHaveType?: string | RegExp;
263
-
264
- /**
265
- * The vuex store that this product should use by default i.e. 'management'
266
- */
267
- inStore?: string;
268
-
269
- /**
270
- * Show the cluster switcher in the navigation
271
- */
272
- showClusterSwitcher?: boolean;
273
-
274
- /**
275
- * Indicates whether UI Extensions can add pages to this product
276
- */
277
- extendable?: boolean;
278
-
279
- /**
280
- * Show the namespace filter in the header
281
- */
282
- showNamespaceFilter?: boolean;
283
-
284
- /**
285
- * A number used to determine where in navigation this item will be placed. The highest number will be at the top of the list.
286
- */
287
- weight?: number;
288
-
289
- /**
290
- * The route that the product will lead to if click on in navigation.
291
- */
292
- to?: PluginRouteRecordRaw;
293
-
294
- /**
295
- * Whether the product can be removed by users (default: false — products are built-in/not removable unless explicitly set to true)
296
- */
297
- removable?: boolean;
298
-
299
- /**
300
- * Alternative to the icon property. Uses require
301
- */
302
- svg?: Function;
303
-
304
- /**
305
- * Product name
306
- */
307
- name?: string;
308
-
309
- /**
310
- * controls whether a workspace switcher dropdown appears in the header (instead of the namespace filter) if set to true
311
- */
312
- showWorkspaceSwitcher?: boolean;
313
-
314
- /**
315
- *
316
- */
317
- label?: string;
318
-
319
- labelKey?: string;
320
-
321
- iconHeader?: string;
322
-
323
- // Do not use - internal use only
324
- version?: number;
325
-
326
- /**
327
- * Leaving these here for completeness but I don't think these should be advertised as useable to plugin creators.
328
- */
329
- // ifHaveVerb: string | RegExp;
330
- // supportRoute: string;
331
- // typeStoreMap: string;
332
- }
333
-
334
199
  /**
335
200
  * Configuration required to show a header in a ResourceTable
336
201
  */
@@ -399,17 +264,6 @@ export interface HeaderOptions {
399
264
  */
400
265
  export type PaginationHeaderOptions = Omit<HeaderOptions, 'getValue'>
401
266
 
402
- export type ResourceTypeConfig = {
403
- options?: {
404
- isCreatable?: boolean;
405
- isEditable?: boolean;
406
- },
407
- listHeaders?: {
408
- legacy?: HeaderOptions[];
409
- paginated?: PaginationHeaderOptions[];
410
- }
411
- };
412
-
413
267
  /**
414
268
  * External extension configuration for @HeaderOptions
415
269
  */
@@ -568,7 +422,7 @@ export interface DSLReturnType {
568
422
  * @param options {@link ProductOptions}
569
423
  * @returns {@link void}
570
424
  */
571
- product: (options: ProductOptions) => void;
425
+ product: (options: TypeMapProduct) => void;
572
426
 
573
427
  /**
574
428
  /**
@@ -691,62 +545,12 @@ export type ModelExtensionContext = {
691
545
  /**
692
546
  * Constructor signature for a model extension
693
547
  */
694
- export type ModelExtensionConstructor = (context: ModelExtensionContext) => Object;
548
+ export type ModelExtensionConstructor = new (context: ModelExtensionContext) => Object;
695
549
 
696
550
  /**
697
551
  * Interface for a UI Extension
698
552
  */
699
- export interface IExtension {
700
- /**
701
- * Register a top-level product as a flag on the plugin
702
- * @internal - DO NOT USE - Internal API only
703
- */
704
- _registerTopLevelProduct(): void;
705
- /**
706
- *
707
- * @internal - DO NOT USE - Internal API only
708
- */
709
- _setStartRouteWithProduct(value: boolean): void;
710
-
711
- /**
712
- * Add a product to the sidebar, with children and a side menu for navigation for internal pages
713
- * @param name
714
- * @param config
715
- */
716
- addProduct(product: ProductMetadata, config: ProductChildPage[]): void;
717
- addProduct(product: ProductMetadata, config: ProductChildGroup[]): void;
718
- addProduct(product: ProductMetadata, config: ProductChild[]): void;
719
-
720
- /**
721
- * Add a product to the sidebar, without children (no side menu, single page only)
722
- * @param product
723
- */
724
- addProduct(product: ProductSinglePage): void;
725
-
726
- /**
727
- * Add a product with just a name (convenience/bridge method for quick setup).
728
- * Creates a basic product with an empty page component automatically.
729
- * This is useful for getting started quickly - expand to the full API once you're ready to add custom pages.
730
- * @param productName Simple product name - will be used as both the name and label
731
- */
732
- addProduct(productName: string): void;
733
-
734
- /**
735
- * Add a product to the sidebar (deprecated, use other signatures of addProduct instead)
736
- * @deprecated Use other `addProduct` signatures instead
737
- * @param importFn Function that will import the module containing a product definition
738
- */
739
- addProduct(importFn: ProductFunction): void;
740
-
741
- /**
742
- * Extend an existing product in Rancher, with children and a side menu for navigation for internal pages
743
- *
744
- * @param product Product to be extended
745
- * @param config Product extension configuration
746
- */
747
- extendProduct(product: StandardProductName | string, config: ProductChildPage[]): void;
748
- extendProduct(product: StandardProductName | string, config: ProductChildGroup[]): void;
749
- extendProduct(product: StandardProductName | string, config: ProductChild[]): void;
553
+ export interface IExtension extends IExtensionProducts {
750
554
 
751
555
  /**
752
556
  * Add a locale to the i18n store
@@ -241,4 +241,56 @@ describe('view: provisioning.cattle.io.cluster', () => {
241
241
  expect(wrapper.vm.fakeMachines).toHaveLength(0);
242
242
  });
243
243
  });
244
+
245
+ describe('computed: showLog', () => {
246
+ it('returns true when mgmt has a log link and extDetailTabs.logs is enabled', async() => {
247
+ const value = { mgmt: { hasLink: (link: string) => link === 'log' } };
248
+
249
+ const wrapper = shallowMount(ProvisioningCattleIoCluster, {
250
+ props: { value },
251
+ global: { mocks },
252
+ });
253
+
254
+ await wrapper.setData({ extDetailTabs: { logs: true } });
255
+
256
+ expect(wrapper.vm.showLog).toStrictEqual(true);
257
+ });
258
+
259
+ it('returns false when mgmt does not have a log link', async() => {
260
+ const value = { mgmt: { hasLink: () => false } };
261
+
262
+ const wrapper = shallowMount(ProvisioningCattleIoCluster, {
263
+ props: { value },
264
+ global: { mocks },
265
+ });
266
+
267
+ await wrapper.setData({ extDetailTabs: { logs: true } });
268
+
269
+ expect(wrapper.vm.showLog).toStrictEqual(false);
270
+ });
271
+
272
+ it('returns false when mgmt has a log link but extDetailTabs.logs is disabled', async() => {
273
+ const value = { mgmt: { hasLink: (link: string) => link === 'log' } };
274
+
275
+ const wrapper = shallowMount(ProvisioningCattleIoCluster, {
276
+ props: { value },
277
+ global: { mocks },
278
+ });
279
+
280
+ await wrapper.setData({ extDetailTabs: { logs: false } });
281
+
282
+ expect(wrapper.vm.showLog).toStrictEqual(false);
283
+ });
284
+
285
+ it('returns false when mgmt is undefined', async() => {
286
+ const value = {};
287
+
288
+ const wrapper = shallowMount(ProvisioningCattleIoCluster, {
289
+ props: { value },
290
+ global: { mocks },
291
+ });
292
+
293
+ expect(wrapper.vm.showLog).toBeFalsy();
294
+ });
295
+ });
244
296
  });
package/detail/pod.vue CHANGED
@@ -38,7 +38,7 @@ export default {
38
38
  const projectId = namespace?.metadata?.labels[PROJECT];
39
39
 
40
40
  if (projectId) {
41
- this.POD_PROJECT_METRICS_DETAIL_URL = `/api/v1/namespaces/cattle-project-${ projectId }-monitoring/services/http:cattle-project-${ projectId }-monitoring-grafana:80/proxy/d/rancher-pod-containers-1/rancher-pod-containers?orgId=1'`;
41
+ this.POD_PROJECT_METRICS_DETAIL_URL = `/api/v1/namespaces/cattle-project-${ projectId }-monitoring/services/http:cattle-project-${ projectId }-monitoring-grafana:80/proxy/d/rancher-pod-containers-1/rancher-pod-containers?orgId=1`;
42
42
  this.POD_PROJECT_METRICS_SUMMARY_URL = `/api/v1/namespaces/cattle-project-${ projectId }-monitoring/services/http:cattle-project-${ projectId }-monitoring-grafana:80/proxy/d/rancher-pod-1/rancher-pod?orgId=1`;
43
43
 
44
44
  this.showProjectMetrics = await allDashboardsExist(this.$store, this.currentCluster.id, [this.POD_PROJECT_METRICS_DETAIL_URL, this.POD_PROJECT_METRICS_SUMMARY_URL], 'cluster', projectId);
@@ -6,7 +6,9 @@ import SortableTable from '@shell/components/SortableTable';
6
6
  import CopyCode from '@shell/components/CopyCode';
7
7
  import Tab from '@shell/components/Tabbed/Tab';
8
8
  import { allHash } from '@shell/utils/promise';
9
- import { CAPI, MANAGEMENT, NORMAN, SNAPSHOT } from '@shell/config/types';
9
+ import {
10
+ CAPI, MANAGEMENT, NORMAN, SNAPSHOT, OPERATION
11
+ } from '@shell/config/types';
10
12
  import {
11
13
  STATE, NAME as NAME_COL, AGE, INTERNAL_EXTERNAL_IP, STATE_NORMAN, ROLES, MACHINE_NODE_OS, MANAGEMENT_NODE_OS, NAME,
12
14
  } from '@shell/config/table-headers';
@@ -149,6 +151,21 @@ export default {
149
151
  fetchOne.snapshots = this.$store.dispatch('management/findAll', { type: SNAPSHOT });
150
152
  }
151
153
 
154
+ // Fetch operation CRDs for clusters with day 2 ops enabled
155
+ if ( this.value.isImportedWithDayTwoOps ) {
156
+ const operationTypes = [
157
+ OPERATION.ETCD_SNAPSHOT,
158
+ OPERATION.ETCD_SNAPSHOT_RESTORE,
159
+ OPERATION.ENCRYPTION_KEY_ROTATE,
160
+ ];
161
+
162
+ for (const opType of operationTypes) {
163
+ if (this.$store.getters['management/canList'](opType)) {
164
+ fetchOne[`operations_${ opType }`] = this.$store.dispatch('management/findAll', { type: opType });
165
+ }
166
+ }
167
+ }
168
+
152
169
  if ( this.value.isImported || this.value.isCustom || this.value.isHostedKubernetesProvider ) {
153
170
  fetchOne.clusterToken = this.value.getOrCreateToken();
154
171
  }
@@ -210,9 +227,7 @@ export default {
210
227
  if ( this.$store.getters['management/canList'](MANAGEMENT.RKE_TEMPLATE) ) {
211
228
  this.$store.dispatch('management/findAll', { type: MANAGEMENT.RKE_TEMPLATE });
212
229
  }
213
- },
214
230
 
215
- created() {
216
231
  if ( this.showLog ) {
217
232
  this.connectLog();
218
233
  }
@@ -292,6 +307,7 @@ export default {
292
307
  registration: true, // in this component
293
308
  snapshots: true, // in this component
294
309
  autoscaler: true, // in this component
310
+ operations: true, // in this component
295
311
  related: true, // in ResourceTabs
296
312
  events: true, // in ResourceTabs
297
313
  conditions: true, // in ResourceTabs
@@ -477,13 +493,54 @@ export default {
477
493
  showSnapshots() {
478
494
  if (this.value.isRke1) {
479
495
  return false;
480
- } else if (this.value.isRke2) {
496
+ } else if (this.value.isRke2 || (this.value.isImported && this.value.isDayTwoOpsEnabled)) {
481
497
  return this.$store.getters['management/canList'](SNAPSHOT) && this.extDetailTabs.snapshots;
482
498
  }
483
499
 
484
500
  return false;
485
501
  },
486
502
 
503
+ showOperations() {
504
+ return this.value.isImportedWithDayTwoOps && this.extDetailTabs.operations;
505
+ },
506
+
507
+ clusterOperations() {
508
+ if (!this.value.isImportedWithDayTwoOps) {
509
+ return [];
510
+ }
511
+
512
+ const mgmtId = this.value.mgmt?.id;
513
+ const operationTypes = [
514
+ OPERATION.ETCD_SNAPSHOT,
515
+ OPERATION.ETCD_SNAPSHOT_RESTORE,
516
+ OPERATION.ENCRYPTION_KEY_ROTATE,
517
+ ];
518
+
519
+ const allOps = [];
520
+
521
+ for (const opType of operationTypes) {
522
+ const resources = this.$store.getters['management/all'](opType) || [];
523
+
524
+ allOps.push(...resources.filter((op) => op.spec?.clusterRef?.name === mgmtId));
525
+ }
526
+
527
+ return allOps;
528
+ },
529
+
530
+ operationHeaders() {
531
+ return [
532
+ STATE,
533
+ NAME,
534
+ {
535
+ name: 'type',
536
+ labelKey: 'tableHeaders.type',
537
+ value: 'type',
538
+ sort: 'type',
539
+ },
540
+ AGE,
541
+ ];
542
+ },
543
+
487
544
  isRke1() {
488
545
  return this.value.isRke1;
489
546
  },
@@ -566,7 +623,10 @@ export default {
566
623
  {
567
624
  ...STATE_NORMAN, value: 'snapshotFile.status', formatterOpts: { arbitrary: true }
568
625
  },
569
- NAME,
626
+ {
627
+ ...NAME,
628
+ formatter: 'EtcdSnapshotName',
629
+ },
570
630
  {
571
631
  name: 'size',
572
632
  labelKey: 'tableHeaders.size',
@@ -1150,6 +1210,20 @@ export default {
1150
1210
  </template>
1151
1211
  </SortableTable>
1152
1212
  </Tab>
1213
+ <Tab
1214
+ v-if="showOperations"
1215
+ name="operations"
1216
+ :label="t('cluster.tabs.operations')"
1217
+ :weight="0"
1218
+ >
1219
+ <SortableTable
1220
+ :headers="operationHeaders"
1221
+ default-sort-by="age"
1222
+ :table-actions="false"
1223
+ :rows="clusterOperations"
1224
+ :search="false"
1225
+ />
1226
+ </Tab>
1153
1227
  <AutoscalerTab
1154
1228
  v-if="showAutoScalerTab"
1155
1229
  :value="value"
@@ -1,5 +1,5 @@
1
1
  <script>
2
- import { SNAPSHOT } from '@shell/config/types';
2
+ import { SNAPSHOT, OPERATION } from '@shell/config/types';
3
3
  import AsyncButton from '@shell/components/AsyncButton';
4
4
  import { Card } from '@components/Card';
5
5
  import { Banner } from '@components/Banner';
@@ -10,7 +10,7 @@ import day from 'dayjs';
10
10
  import { escapeHtml } from '@shell/utils/string';
11
11
  import { DATE_FORMAT, TIME_FORMAT } from '@shell/store/prefs';
12
12
  import { set } from '@shell/utils/object';
13
-
13
+ import { createOperationCR } from '@shell/utils/operation-cr';
14
14
  export default {
15
15
  emits: ['close'],
16
16
 
@@ -66,6 +66,10 @@ export default {
66
66
  async getEtcdBackups() {
67
67
  let etcdBackups = await this.$store.dispatch('management/findAll', { type: SNAPSHOT });
68
68
 
69
+ if (this.cluster.isImportedWithDayTwoOps) {
70
+ return etcdBackups
71
+ .filter((backup) => backup.metadata.namespace === this.cluster.mgmt?.id && backup.spec?.clusterName === this.cluster.mgmt?.metadata?.name );
72
+ }
69
73
  etcdBackups = etcdBackups.filter((backup) => backup.clusterId === this.cluster.id);
70
74
 
71
75
  return etcdBackups;
@@ -82,12 +86,32 @@ export default {
82
86
 
83
87
  async apply(buttonDone) {
84
88
  try {
85
- const currentGeneration = this.cluster.spec?.rkeConfig?.rotateEncryptionKeys?.generation || 0;
86
-
87
- // To rotate the encryption keys, increment
88
- // rkeConfig.rotateEncyrptionKeys.generation in the YAML.
89
- set(this.cluster, 'spec.rkeConfig.rotateEncryptionKeys.generation', currentGeneration + 1);
90
- await this.cluster.save();
89
+ const isImportedWithDayTwoOps = this.cluster?.isImportedWithDayTwoOps || this.cluster?.mgmt?.isDayTwoOpsEnabled;
90
+
91
+ if (isImportedWithDayTwoOps) {
92
+ if (!isImportedWithDayTwoOps) {
93
+ throw new Error(this.t('promptRotateEncryptionKey.error.unableToResolveTargetCluster'));
94
+ }
95
+ // For imported clusters with day 2 ops, create an encryption key rotation operation CR
96
+ const namespace = this.cluster.mgmt?.id;
97
+ const safePrefix = this.cluster.mgmt?.id;
98
+ const spec = {
99
+ clusterRef: {
100
+ apiVersion: 'management.cattle.io/v3',
101
+ kind: 'Cluster',
102
+ name: this.cluster.mgmt?.id,
103
+ }
104
+ };
105
+
106
+ createOperationCR(this.$store.dispatch, OPERATION.ENCRYPTION_KEY_ROTATE, spec, namespace, safePrefix);
107
+ } else {
108
+ const currentGeneration = this.cluster.spec?.rkeConfig?.rotateEncryptionKeys?.generation || 0;
109
+
110
+ // To rotate the encryption keys, increment
111
+ // rkeConfig.rotateEncyrptionKeys.generation in the YAML.
112
+ set(this.cluster, 'spec.rkeConfig.rotateEncryptionKeys.generation', currentGeneration + 1);
113
+ await this.cluster.save();
114
+ }
91
115
 
92
116
  this.close(buttonDone);
93
117
  } catch (err) {
@@ -128,7 +152,7 @@ export default {
128
152
  <Banner
129
153
  v-else
130
154
  color="error"
131
- label-key="promptRotateEncryptionKey.error"
155
+ label-key="promptRotateEncryptionKey.error.noBackup"
132
156
  />
133
157
  </div>
134
158
  </div>
@@ -0,0 +1,78 @@
1
+ import { shallowMount } from '@vue/test-utils';
2
+ import RotateEncryptionKeyDialog from '@shell/dialog/RotateEncryptionKeyDialog.vue';
3
+ import { OPERATION } from '@shell/config/types';
4
+ import { createOperationCR } from '@shell/utils/operation-cr';
5
+
6
+ jest.mock('@shell/utils/operation-cr', () => ({ createOperationCR: jest.fn() }));
7
+
8
+ describe('component: RotateEncryptionKeyDialog', () => {
9
+ const t = (key: string) => key;
10
+
11
+ const createWrapper = (cluster: any, dispatch = jest.fn()) => {
12
+ return shallowMount(RotateEncryptionKeyDialog, {
13
+ propsData: { cluster },
14
+ global: {
15
+ mocks: {
16
+ t,
17
+ $store: {
18
+ dispatch,
19
+ getters: {
20
+ isRancher: true,
21
+ 'prefs/get': jest.fn((key: string) => (key.includes('date') ? 'YYYY-MM-DD' : 'HH:mm:ss')),
22
+ }
23
+ },
24
+ },
25
+ directives: { 'clean-html': true }
26
+ }
27
+ });
28
+ };
29
+
30
+ beforeEach(() => {
31
+ jest.clearAllMocks();
32
+ });
33
+
34
+ it('should create operation CR for imported day 2 ops clusters', async() => {
35
+ (createOperationCR as jest.Mock).mockResolvedValue(undefined);
36
+
37
+ const cluster = {
38
+ isImportedWithDayTwoOps: true,
39
+ mgmt: { id: 'c-m-1' },
40
+ save: jest.fn(),
41
+ spec: { rkeConfig: {} }
42
+ };
43
+ const dispatch = jest.fn();
44
+ const buttonDone = jest.fn();
45
+ const wrapper = createWrapper(cluster, dispatch);
46
+
47
+ await wrapper.vm.apply(buttonDone);
48
+
49
+ expect(createOperationCR).toHaveBeenCalledWith(dispatch, OPERATION.ENCRYPTION_KEY_ROTATE, {
50
+ clusterRef: {
51
+ apiVersion: 'management.cattle.io/v3',
52
+ kind: 'Cluster',
53
+ name: 'c-m-1',
54
+ }
55
+ }, 'c-m-1', 'c-m-1');
56
+ expect(cluster.save).not.toHaveBeenCalled();
57
+ expect(buttonDone).toHaveBeenCalledWith(true);
58
+ });
59
+
60
+ it('should update generation and save for non-day-2 clusters', async() => {
61
+ const save = jest.fn().mockResolvedValue(undefined);
62
+ const cluster = {
63
+ isImportedWithDayTwoOps: false,
64
+ mgmt: { id: 'c-m-1' },
65
+ save,
66
+ spec: { rkeConfig: { rotateEncryptionKeys: { generation: 4 } } }
67
+ };
68
+ const buttonDone = jest.fn();
69
+ const wrapper = createWrapper(cluster);
70
+
71
+ await wrapper.vm.apply(buttonDone);
72
+
73
+ expect(createOperationCR).not.toHaveBeenCalled();
74
+ expect(cluster.spec.rkeConfig.rotateEncryptionKeys.generation).toBe(5);
75
+ expect(save).toHaveBeenCalledWith();
76
+ expect(buttonDone).toHaveBeenCalledWith(true);
77
+ });
78
+ });