@rancher/shell 3.0.9-rc.3 → 3.0.9-rc.5

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 (128) hide show
  1. package/assets/brand/suse/metadata.json +2 -1
  2. package/assets/translations/en-us.yaml +105 -5
  3. package/components/ActionMenuShell.vue +1 -1
  4. package/components/Inactivity.vue +2 -2
  5. package/components/Resource/Detail/Card/ExtrasCard.vue +49 -15
  6. package/components/Resource/Detail/Card/__tests__/ExtrasCard.test.ts +111 -0
  7. package/components/Resource/Detail/Masthead/__tests__/index.test.ts +0 -17
  8. package/components/Resource/Detail/Masthead/index.vue +11 -4
  9. package/components/Resource/Detail/Metadata/IdentifyingInformation/index.vue +3 -1
  10. package/components/Resource/Detail/Metadata/index.vue +1 -1
  11. package/components/Resource/Detail/ResourceRow.vue +1 -1
  12. package/components/ResourceDetail/Masthead/latest.vue +12 -2
  13. package/components/ResourceList/index.vue +9 -0
  14. package/components/ResourceTable.vue +38 -4
  15. package/components/Tabbed/Tab.vue +4 -0
  16. package/components/Tabbed/index.vue +4 -1
  17. package/components/__tests__/ProjectRow.test.ts +60 -0
  18. package/components/form/ChangePassword.vue +41 -35
  19. package/components/form/ResourceQuota/Project.vue +42 -1
  20. package/components/form/ResourceQuota/ProjectRow.vue +71 -4
  21. package/components/form/ResourceQuota/__tests__/Project.test.ts +63 -0
  22. package/components/form/SelectOrCreateAuthSecret.vue +6 -1
  23. package/components/form/__tests__/SelectOrCreateAuthSecret.test.ts +35 -0
  24. package/components/formatter/KubeconfigClusters.vue +74 -0
  25. package/components/formatter/MachineSummaryGraph.vue +10 -2
  26. package/components/formatter/__tests__/KubeconfigClusters.test.ts +125 -0
  27. package/components/nav/TopLevelMenu.helper.ts +50 -2
  28. package/components/nav/TopLevelMenu.vue +14 -0
  29. package/components/nav/Type.vue +5 -0
  30. package/components/nav/__tests__/TopLevelMenu.test.ts +3 -3
  31. package/components/nav/__tests__/Type.test.ts +6 -4
  32. package/config/product/explorer.js +4 -3
  33. package/config/product/manager.js +47 -3
  34. package/config/router/navigation-guards/authentication.js +8 -9
  35. package/config/router/routes.js +4 -1
  36. package/config/types.js +10 -2
  37. package/detail/auditlog.cattle.io.auditpolicy.vue +19 -0
  38. package/detail/management.cattle.io.user.vue +1 -2
  39. package/detail/node.vue +0 -1
  40. package/detail/provisioning.cattle.io.cluster.vue +2 -1
  41. package/dialog/ChangePasswordDialog.vue +8 -0
  42. package/dialog/GenericPrompt.vue +20 -3
  43. package/dialog/ScaleMachineDownDialog.vue +65 -15
  44. package/dialog/SearchDialog.vue +10 -2
  45. package/dialog/__tests__/ScaleMachineDownDialog.test.ts +184 -0
  46. package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +89 -0
  47. package/edit/__tests__/management.cattle.io.project.test.js +56 -1
  48. package/edit/auditlog.cattle.io.auditpolicy/AdditionalRedactions.vue +114 -0
  49. package/edit/auditlog.cattle.io.auditpolicy/Filters.vue +119 -0
  50. package/edit/auditlog.cattle.io.auditpolicy/General.vue +180 -0
  51. package/edit/auditlog.cattle.io.auditpolicy/__tests__/AdditionalRedactions.test.ts +327 -0
  52. package/edit/auditlog.cattle.io.auditpolicy/__tests__/Filters.test.ts +449 -0
  53. package/edit/auditlog.cattle.io.auditpolicy/__tests__/General.test.ts +472 -0
  54. package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/AdditionalRedactions.test.ts.snap +27 -0
  55. package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/Filters.test.ts.snap +39 -0
  56. package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/General.test.ts.snap +174 -0
  57. package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/index.test.ts.snap +29 -0
  58. package/edit/auditlog.cattle.io.auditpolicy/__tests__/index.test.ts +215 -0
  59. package/edit/auditlog.cattle.io.auditpolicy/index.vue +104 -0
  60. package/edit/auditlog.cattle.io.auditpolicy/types.ts +28 -0
  61. package/edit/fleet.cattle.io.gitrepo.vue +16 -1
  62. package/edit/management.cattle.io.project.vue +8 -2
  63. package/edit/management.cattle.io.user.vue +29 -34
  64. package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +178 -0
  65. package/edit/provisioning.cattle.io.cluster/rke2.vue +22 -2
  66. package/edit/provisioning.cattle.io.cluster/shared.ts +4 -0
  67. package/edit/provisioning.cattle.io.cluster/tabs/Basics.vue +1 -0
  68. package/edit/provisioning.cattle.io.cluster/tabs/etcd/S3Config.vue +57 -2
  69. package/edit/provisioning.cattle.io.cluster/tabs/etcd/__tests__/S3Config.test.ts +109 -0
  70. package/edit/provisioning.cattle.io.cluster/tabs/etcd/index.vue +1 -0
  71. package/list/auditlog.cattle.io.auditpolicy.vue +63 -0
  72. package/list/ext.cattle.io.kubeconfig.vue +118 -0
  73. package/list/group.principal.vue +11 -15
  74. package/list/management.cattle.io.user.vue +11 -21
  75. package/machine-config/azure.vue +14 -0
  76. package/mixins/__tests__/chart.test.ts +147 -0
  77. package/mixins/browser-tab-visibility.js +5 -4
  78. package/mixins/chart.js +10 -8
  79. package/mixins/fetch.client.js +6 -0
  80. package/models/__tests__/auditlog.cattle.io.auditpolicy.test.ts +117 -0
  81. package/models/__tests__/ext.cattle.io.kubeconfig.test.ts +364 -0
  82. package/models/__tests__/secret.test.ts +55 -0
  83. package/models/__tests__/workload.test.ts +49 -6
  84. package/models/auditlog.cattle.io.auditpolicy.js +46 -0
  85. package/models/cluster.x-k8s.io.machine.js +1 -1
  86. package/models/cluster.x-k8s.io.machinedeployment.js +5 -5
  87. package/models/event.js +5 -0
  88. package/models/ext.cattle.io.groupmembershiprefreshrequest.js +15 -0
  89. package/models/ext.cattle.io.kubeconfig.ts +97 -0
  90. package/models/ext.cattle.io.passwordchangerequest.js +15 -0
  91. package/models/ext.cattle.io.selfuser.js +15 -0
  92. package/models/fleet-application.js +17 -7
  93. package/models/management.cattle.io.user.js +28 -31
  94. package/models/schema.js +18 -0
  95. package/models/secret.js +28 -25
  96. package/models/steve-schema.ts +39 -2
  97. package/models/workload.js +3 -2
  98. package/package.json +2 -2
  99. package/pages/about.vue +3 -2
  100. package/pages/account/index.vue +23 -16
  101. package/pages/auth/login.vue +15 -8
  102. package/pages/auth/setup.vue +52 -15
  103. package/pages/c/_cluster/apps/charts/AppChartCardFooter.vue +38 -14
  104. package/pages/c/_cluster/apps/charts/index.vue +1 -0
  105. package/pages/home.vue +9 -3
  106. package/plugins/dashboard-store/__tests__/resource-class.test.ts +1 -3
  107. package/plugins/dashboard-store/actions.js +7 -0
  108. package/plugins/dashboard-store/getters.js +23 -1
  109. package/plugins/dashboard-store/index.js +3 -2
  110. package/plugins/dashboard-store/mutations.js +4 -0
  111. package/plugins/dashboard-store/resource-class.js +12 -5
  112. package/plugins/steve/__tests__/steve-class.test.ts +167 -0
  113. package/plugins/steve/schema.d.ts +5 -0
  114. package/plugins/steve/steve-class.js +19 -0
  115. package/plugins/steve/steve-pagination-utils.ts +2 -1
  116. package/rancher-components/RcItemCard/RcItemCard.test.ts +4 -2
  117. package/rancher-components/RcItemCard/RcItemCard.vue +27 -10
  118. package/store/auth.js +57 -19
  119. package/store/notifications.ts +1 -1
  120. package/store/type-map.js +12 -1
  121. package/types/shell/index.d.ts +24 -15
  122. package/types/store/dashboard-store.types.ts +7 -0
  123. package/utils/__tests__/chart.test.ts +96 -0
  124. package/utils/__tests__/version.test.ts +1 -19
  125. package/utils/chart.js +64 -0
  126. package/utils/pagination-wrapper.ts +11 -3
  127. package/utils/version.js +5 -17
  128. package/vue.config.js +26 -13
@@ -0,0 +1,118 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue';
3
+ import { useStore } from 'vuex';
4
+ import PaginatedResourceTable from '@shell/components/PaginatedResourceTable.vue';
5
+ import { CAPI, MANAGEMENT } from '@shell/config/types';
6
+ import { ActionFindPageArgs } from '@shell/types/store/dashboard-store.types';
7
+ import { FilterArgs, PaginationFilterField, PaginationParamFilter } from '@shell/types/store/pagination.types';
8
+ import { PagTableFetchPageSecondaryResourcesOpts, PagTableFetchSecondaryResourcesOpts, PagTableFetchSecondaryResourcesReturns } from '@shell/types/components/paginatedResourceTable';
9
+
10
+ defineProps({
11
+ schema: {
12
+ type: Object,
13
+ required: true
14
+ },
15
+
16
+ useQueryParamsForSimpleFiltering: {
17
+ type: Boolean,
18
+ default: false
19
+ }
20
+ });
21
+
22
+ const store = useStore();
23
+
24
+ const canViewProvClusters = computed<boolean>(() => {
25
+ return !!store.getters['management/canList'](CAPI.RANCHER_CLUSTER);
26
+ });
27
+
28
+ const canViewMgmtClusters = computed<boolean>(() => {
29
+ return !!store.getters['management/canList'](MANAGEMENT.CLUSTER);
30
+ });
31
+
32
+ /**
33
+ * Fetch all clusters when not using pagination
34
+ */
35
+ async function fetchSecondaryResources({ canPaginate }: PagTableFetchSecondaryResourcesOpts): PagTableFetchSecondaryResourcesReturns {
36
+ if (canPaginate) {
37
+ return;
38
+ }
39
+
40
+ const promises = [];
41
+
42
+ if (canViewProvClusters.value) {
43
+ promises.push(store.dispatch('management/findAll', { type: CAPI.RANCHER_CLUSTER }));
44
+ }
45
+
46
+ if (canViewMgmtClusters.value) {
47
+ promises.push(store.dispatch('management/findAll', { type: MANAGEMENT.CLUSTER }));
48
+ }
49
+
50
+ await Promise.all(promises);
51
+ }
52
+
53
+ /**
54
+ * Fetch only the clusters referenced by kubeconfigs on the current page
55
+ *
56
+ * NOTE: For the time being this isn't validated because ext.cattle.io.kubeconfig is not one of the indexed resources. I'm putting this in for future support since secondary resources are needed.
57
+ */
58
+ async function fetchPageSecondaryResources({ force, page }: PagTableFetchPageSecondaryResourcesOpts) {
59
+ if (!page?.length) {
60
+ return;
61
+ }
62
+
63
+ const uniqueClusterIds = new Set<string>();
64
+
65
+ page.forEach((kubeconfig: any) => {
66
+ const ids = kubeconfig.spec?.clusters || [];
67
+
68
+ ids.forEach((id: string) => uniqueClusterIds.add(id));
69
+ });
70
+
71
+ if (uniqueClusterIds.size === 0) {
72
+ return;
73
+ }
74
+
75
+ const clusterIdArray = Array.from(uniqueClusterIds);
76
+
77
+ if (canViewProvClusters.value) {
78
+ const opt: ActionFindPageArgs = {
79
+ force,
80
+ pagination: new FilterArgs({
81
+ filters: PaginationParamFilter.createMultipleFields(
82
+ clusterIdArray.map((id) => new PaginationFilterField({
83
+ field: 'status.clusterName', // Verified it's one of the attribute fields listed in the schema, according to steve-pagination-utils that means it should be filterable
84
+ value: id
85
+ }))
86
+ )
87
+ })
88
+ };
89
+
90
+ store.dispatch('management/findPage', { type: CAPI.RANCHER_CLUSTER, opt });
91
+ }
92
+
93
+ if (canViewMgmtClusters.value) {
94
+ const opt: ActionFindPageArgs = {
95
+ force,
96
+ pagination: new FilterArgs({
97
+ filters: PaginationParamFilter.createMultipleFields(
98
+ clusterIdArray.map((id) => new PaginationFilterField({
99
+ field: 'metadata.name', // Verified it's one of the attribute fields listed in the schema, according to steve-pagination-utils that means it should be filterable
100
+ value: id
101
+ }))
102
+ )
103
+ })
104
+ };
105
+
106
+ store.dispatch('management/findPage', { type: MANAGEMENT.CLUSTER, opt });
107
+ }
108
+ }
109
+ </script>
110
+
111
+ <template>
112
+ <PaginatedResourceTable
113
+ :schema="schema"
114
+ :use-query-params-for-simple-filtering="useQueryParamsForSimpleFiltering"
115
+ :fetch-secondary-resources="fetchSecondaryResources"
116
+ :fetch-page-secondary-resources="fetchPageSecondaryResources"
117
+ />
118
+ </template>
@@ -2,14 +2,13 @@
2
2
  import ResourceTable from '@shell/components/ResourceTable';
3
3
  import Loading from '@shell/components/Loading';
4
4
  import Masthead from '@shell/components/ResourceList/Masthead';
5
- import { NORMAN, MANAGEMENT } from '@shell/config/types';
5
+ import { NORMAN, MANAGEMENT, EXT } from '@shell/config/types';
6
6
  import AsyncButton from '@shell/components/AsyncButton';
7
7
  import { applyProducts } from '@shell/store/type-map';
8
8
  import { NAME } from '@shell/config/product/auth';
9
9
  import { MODE, _EDIT } from '@shell/config/query-params';
10
10
  import { mapState } from 'vuex';
11
11
  import { BLANK_CLUSTER } from '@shell/store/store-types.js';
12
- import { allHash } from '@shell/utils/promise';
13
12
 
14
13
  export default {
15
14
  components: {
@@ -37,21 +36,20 @@ export default {
37
36
  const authConfigSchema = this.$store.getters[`management/schemaFor`](MANAGEMENT.AUTH_CONFIG);
38
37
  const grbSchema = this.$store.getters['rancher/schemaFor'](NORMAN.GLOBAL_ROLE_BINDING);
39
38
 
40
- const hash = await allHash({
41
- user: this.$store.dispatch('rancher/request', { url: '/v3/users?limit=0' }),
42
- providers: authConfigSchema ? this.$store.dispatch(`management/findAll`, { type: MANAGEMENT.AUTH_CONFIG }) : Promise.resolve([])
43
- });
39
+ const providers = authConfigSchema ? await this.$store.dispatch(`management/findAll`, { type: MANAGEMENT.AUTH_CONFIG }) : [];
44
40
 
45
- const nonLocalAuthProvider = !!hash.providers.find((p) => p.name !== 'local' && p.enabled === true);
41
+ const nonLocalAuthProvider = !!providers.find((p) => p.name !== 'local' && p.enabled === true);
46
42
 
47
- this.canRefreshAccess = nonLocalAuthProvider && !!hash.user?.actions?.refreshauthprovideraccess;
43
+ this.membershipRefreshRequests = await this.$store.dispatch('management/create', { type: EXT.GROUP_MEMBERSHIP_REFRESH_REQUESTS });
44
+ this.canRefreshMemberships = !!this.membershipRefreshRequests?.canRefreshMemberships;
48
45
  this.canCreateGlobalRoleBinding = nonLocalAuthProvider && grbSchema?.collectionMethods?.includes('POST');
49
46
  },
50
47
  data() {
51
48
  return {
52
49
  rows: [],
50
+ membershipRefreshRequests: undefined,
53
51
  canCreateGlobalRoleBinding: false,
54
- canRefreshAccess: false,
52
+ canRefreshMemberships: false,
55
53
  assignLocation: {
56
54
  path: `/c/${ BLANK_CLUSTER }/${ NAME }/${ NORMAN.SPOOFED.GROUP_PRINCIPAL }/assign-edit`,
57
55
  query: { [MODE]: _EDIT }
@@ -86,11 +84,9 @@ export default {
86
84
  },
87
85
  async refreshGroupMemberships(buttonDone) {
88
86
  try {
89
- await this.$store.dispatch('rancher/request', {
90
- url: '/v3/users?action=refreshauthprovideraccess',
91
- method: 'post',
92
- data: { },
93
- });
87
+ // userId specifies the user ID. Use '*' for all users. Check the schemaDefinition for more details.
88
+ this.membershipRefreshRequests.spec = { userId: '*' };
89
+ await this.membershipRefreshRequests.save();
94
90
 
95
91
  await this.updateGroupPrincipals(true);
96
92
 
@@ -125,7 +121,7 @@ export default {
125
121
  >
126
122
  <template #extraActions>
127
123
  <AsyncButton
128
- v-if="canRefreshAccess"
124
+ v-if="canRefreshMemberships"
129
125
  mode="refresh"
130
126
  :action-label="t('authGroups.actions.refresh')"
131
127
  :waiting-label="t('authGroups.actions.refresh')"
@@ -1,6 +1,6 @@
1
1
  <script>
2
2
  import AsyncButton from '@shell/components/AsyncButton';
3
- import { NORMAN } from '@shell/config/types';
3
+ import { EXT } from '@shell/config/types';
4
4
  import { NAME } from '@shell/config/product/auth';
5
5
  import ResourceTable from '@shell/components/ResourceTable';
6
6
  import Masthead from '@shell/components/ResourceList/Masthead';
@@ -38,14 +38,10 @@ export default {
38
38
  }
39
39
  },
40
40
  async fetch() {
41
- const store = this.$store;
42
-
43
- await store.dispatch(`rancher/findAll`, { type: NORMAN.USER });
44
-
45
41
  await this.$fetchType(this.resource);
46
42
 
47
- this.canRefreshAccess = await this.$store.dispatch('rancher/request', { url: '/v3/users?limit=0' })
48
- .then((res) => !!res?.actions?.refreshauthprovideraccess);
43
+ this.membershipRefreshRequests = await this.$store.dispatch('management/create', { type: EXT.GROUP_MEMBERSHIP_REFRESH_REQUESTS });
44
+ this.canRefreshMemberships = !!this.membershipRefreshRequests?.canRefreshMemberships;
49
45
  },
50
46
 
51
47
  data() {
@@ -55,7 +51,8 @@ export default {
55
51
 
56
52
  return {
57
53
  schema,
58
- canRefreshAccess: false,
54
+ membershipRefreshRequests: undefined,
55
+ canRefreshMemberships: false
59
56
  };
60
57
  },
61
58
 
@@ -82,28 +79,21 @@ export default {
82
79
  // 1) Only show system users in explorer/users and not in auth/users
83
80
  // 2) Supplement user with info to enable/disable the refresh group membership action (this is not persisted on save)
84
81
  const params = { ...this.$route.params };
85
- const requiredUsers = params.product === NAME ? this.rows.filter((a) => !a.isSystem) : this.rows;
86
-
87
- requiredUsers.forEach((r) => {
88
- r.canRefreshAccess = this.canRefreshAccess;
89
- });
90
82
 
91
- return requiredUsers;
83
+ return params.product === NAME ? this.rows.filter((a) => !a.isSystem) : this.rows;
92
84
  },
93
85
 
94
86
  isAdmin() {
95
87
  return isAdminUser(this.$store.getters);
96
- },
88
+ }
97
89
  },
98
90
 
99
91
  methods: {
100
92
  async refreshGroupMemberships(buttonDone) {
101
93
  try {
102
- await this.$store.dispatch('rancher/collectionAction', {
103
- type: NORMAN.USER,
104
- actionName: 'refreshauthprovideraccess',
105
- });
106
-
94
+ // userId specifies the user ID. Use '*' for all users. Check the schemaDefinition for more details.
95
+ this.membershipRefreshRequests.spec = { userId: '*' };
96
+ await this.membershipRefreshRequests.save();
107
97
  buttonDone(true);
108
98
  } catch (err) {
109
99
  this.$store.dispatch('growl/fromError', { title: this.t('user.list.errorRefreshingGroupMemberships'), err }, { root: true });
@@ -125,7 +115,7 @@ export default {
125
115
  >
126
116
  <template #extraActions>
127
117
  <AsyncButton
128
- v-if="canRefreshAccess"
118
+ v-if="canRefreshMemberships"
129
119
  mode="refresh"
130
120
  :action-label="t('authGroups.actions.refresh')"
131
121
  :waiting-label="t('authGroups.actions.refresh')"
@@ -209,6 +209,10 @@ export default {
209
209
  }
210
210
  },
211
211
 
212
+ setup() {
213
+ return { _EDIT };
214
+ },
215
+
212
216
  data() {
213
217
  return {
214
218
  azureEnvironments,
@@ -518,6 +522,11 @@ export default {
518
522
  </div>
519
523
  </div>
520
524
  <div v-else>
525
+ <Banner
526
+ v-if="mode === _EDIT && !value.managedDisks"
527
+ color="warning"
528
+ :label="t('cluster.machineConfig.azure.managedDisks.deprecationWarning', {}, true)"
529
+ />
521
530
  <div class="row mt-20">
522
531
  <div class="col span-6">
523
532
  <LabeledSelect
@@ -843,6 +852,11 @@ export default {
843
852
  :label="t('cluster.machineConfig.azure.managedDisks.label')"
844
853
  :disabled="disabled"
845
854
  />
855
+ <Banner
856
+ v-if="!value.managedDisks"
857
+ color="warning"
858
+ :label="t('cluster.machineConfig.azure.managedDisks.deprecationWarning', {}, true)"
859
+ />
846
860
  <Banner
847
861
  v-if="value.availabilityZone && !value.managedDisks"
848
862
  color="error"
@@ -322,5 +322,152 @@ describe('chartMixin', () => {
322
322
  icon: 'icon-upgrade-alt',
323
323
  });
324
324
  });
325
+
326
+ it('should return "upgrade" action when upgrading from a pre-release to a stable version with "up" build metadata', () => {
327
+ const wrapper = mount(DummyComponent, {
328
+ data: () => ({
329
+ existing: { spec: { chart: { metadata: { version: '108.0.0+up0.25.0-rc.4' } } } },
330
+ version: { version: '108.0.0+up0.25.0' }
331
+ }),
332
+ global: {
333
+ mocks: {
334
+ $store: mockStore,
335
+ $route: { query: {} }
336
+ }
337
+ }
338
+ });
339
+
340
+ expect(wrapper.vm.action).toStrictEqual({
341
+ name: 'upgrade',
342
+ tKey: 'upgrade',
343
+ icon: 'icon-upgrade-alt',
344
+ });
345
+ });
346
+
347
+ it('should return "upgrade" action when upgrading with build metadata change', () => {
348
+ const wrapper = mount(DummyComponent, {
349
+ data: () => ({
350
+ existing: { spec: { chart: { metadata: { version: '1.0.0+1' } } } },
351
+ version: { version: '1.0.0+2' }
352
+ }),
353
+ global: {
354
+ mocks: {
355
+ $store: mockStore,
356
+ $route: { query: {} }
357
+ }
358
+ }
359
+ });
360
+
361
+ expect(wrapper.vm.action).toStrictEqual({
362
+ name: 'upgrade',
363
+ tKey: 'upgrade',
364
+ icon: 'icon-upgrade-alt',
365
+ });
366
+ });
367
+ });
368
+
369
+ describe('mappedVersions', () => {
370
+ it('should return versions sorted by semver (descending)', () => {
371
+ const versions = [
372
+ { version: '0.1.0', created: '2026-01-01' },
373
+ { version: '0.2.0-rc1', created: '2026-01-01' },
374
+ { version: '0.2.0', created: '2026-01-01' },
375
+ { version: '1.2.3', created: '2026-01-01' },
376
+ { version: '1.2.3-dev', created: '2026-01-01' },
377
+ { version: '10.0.0', created: '2026-01-01' },
378
+ { version: '2.0.0', created: '2026-01-01' },
379
+ { version: '2.0.0-rc2', created: '2026-01-01' },
380
+ { version: '2.0.0-rc1', created: '2026-01-01' },
381
+ { version: '2.0.0-beta.1', created: '2026-01-01' },
382
+ { version: '2.0.0-alpha', created: '2026-01-01' },
383
+ { version: '3.0.0-rc.3', created: '2026-01-01' },
384
+ { version: '3.0.0-rc.2', created: '2026-01-01' },
385
+ { version: '3.0.0-rc.10', created: '2026-01-01' },
386
+ { version: '108.0.0+up0.25.0-rc.4', created: '2026-01-01' },
387
+ { version: '108.0.0+up0.25.0', created: '2026-01-01' },
388
+ { version: '1.0.0-alpha.beta', created: '2026-01-01' },
389
+ { version: '1.0.0-alpha.1', created: '2026-01-01' },
390
+ { version: '1.0.0-alpha.2', created: '2026-01-01' },
391
+ { version: '1.0.0-alpha', created: '2026-01-01' },
392
+ { version: '1.0.0-beta.11', created: '2026-01-01' },
393
+ { version: '1.0.0-beta.2', created: '2026-01-01' },
394
+ { version: '1.0.0-beta', created: '2026-01-01' },
395
+ { version: '1.0.0+build.1', created: '2026-01-01' },
396
+ { version: '1.0.0+build.2', created: '2026-01-01' },
397
+ { version: '1.0.0+up1.0.0', created: '2026-01-01' },
398
+ { version: '1.0.0+upFoo', created: '2026-01-01' },
399
+ { version: '108.0.0+up0.25.0-rc.5', created: '2026-01-01' },
400
+ { version: '108.0.0+up0.25.1', created: '2026-01-01' },
401
+ { version: '0.0.1', created: '2026-01-01' }
402
+ ];
403
+
404
+ const mockStore = {
405
+ dispatch: jest.fn(() => Promise.resolve()),
406
+ getters: {
407
+ currentCluster: () => {},
408
+ isRancher: () => true,
409
+ 'catalog/repo': () => () => 'repo',
410
+ 'catalog/chart': () => ({ versions }),
411
+ 'prefs/get': () => (key: string) => true,
412
+ 'i18n/t': () => jest.fn()
413
+ }
414
+ };
415
+
416
+ const DummyComponent = {
417
+ mixins: [ChartMixin],
418
+ template: '<div></div>',
419
+ };
420
+
421
+ const wrapper = mount(
422
+ DummyComponent,
423
+ {
424
+ data() {
425
+ return { chart: { versions } };
426
+ },
427
+ global: {
428
+ mocks: {
429
+ $store: mockStore,
430
+ $route: { query: { version: '10.0.0' } }
431
+ }
432
+ }
433
+ });
434
+
435
+ // mappedVersions is a computed property, so we access it directly
436
+ const result = wrapper.vm.mappedVersions;
437
+ const resultVersions = result.map((v: any) => v.version);
438
+
439
+ expect(resultVersions).toStrictEqual([
440
+ '108.0.0+up0.25.1',
441
+ '108.0.0+up0.25.0',
442
+ '108.0.0+up0.25.0-rc.5',
443
+ '108.0.0+up0.25.0-rc.4',
444
+ '10.0.0',
445
+ '3.0.0-rc.10',
446
+ '3.0.0-rc.3',
447
+ '3.0.0-rc.2',
448
+ '2.0.0',
449
+ '2.0.0-rc2',
450
+ '2.0.0-rc1',
451
+ '2.0.0-beta.1',
452
+ '2.0.0-alpha',
453
+ '1.2.3',
454
+ '1.2.3-dev',
455
+ '1.0.0+up1.0.0',
456
+ '1.0.0+upFoo',
457
+ '1.0.0+build.2',
458
+ '1.0.0+build.1',
459
+ '1.0.0-beta.11',
460
+ '1.0.0-beta.2',
461
+ '1.0.0-beta',
462
+ '1.0.0-alpha.beta',
463
+ '1.0.0-alpha.2',
464
+ '1.0.0-alpha.1',
465
+ '1.0.0-alpha',
466
+ '0.2.0',
467
+ '0.2.0-rc1',
468
+ '0.1.0',
469
+ '0.0.1'
470
+ ]);
471
+ });
325
472
  });
326
473
  });
@@ -1,4 +1,4 @@
1
- import { NORMAN } from '@shell/config/types';
1
+ import { EXT } from '@shell/config/types';
2
2
  import { mapGetters } from 'vuex';
3
3
 
4
4
  export default {
@@ -14,9 +14,10 @@ export default {
14
14
 
15
15
  async visibilityChange() {
16
16
  if (!document.hidden) {
17
- await this.$store.dispatch('rancher/request', {
18
- type: NORMAN.USER,
19
- opt: { url: '/v3/users?me=true' }
17
+ await this.$store.dispatch('management/request', {
18
+ url: `/v1/${ EXT.SELFUSER }`,
19
+ method: 'POST',
20
+ data: {}
20
21
  });
21
22
  }
22
23
  },
package/mixins/chart.js CHANGED
@@ -10,7 +10,8 @@ import { NAME as MANAGER } from '@shell/config/product/manager';
10
10
  import { OPA_GATE_KEEPER_ID } from '@shell/pages/c/_cluster/gatekeeper/index.vue';
11
11
  import { formatSi, parseSi } from '@shell/utils/units';
12
12
  import { CAPI, CATALOG } from '@shell/config/types';
13
- import { isPrerelease, compare, isUpgradeFromPreToStable } from '@shell/utils/version';
13
+ import { isPrerelease } from '@shell/utils/version';
14
+ import { compareChartVersions } from '@shell/utils/chart';
14
15
  import difference from 'lodash/difference';
15
16
  import { LINUX, APP_UPGRADE_STATUS } from '@shell/store/catalog';
16
17
  import { clone } from '@shell/utils/object';
@@ -51,7 +52,12 @@ export default {
51
52
  },
52
53
 
53
54
  mappedVersions() {
54
- const versions = this.chart?.versions || [];
55
+ const versions = (this.chart?.versions || []).slice();
56
+
57
+ versions.sort((a, b) => {
58
+ return compareChartVersions(b.version, a.version);
59
+ });
60
+
55
61
  const selectedVersion = this.targetVersion;
56
62
  const OSs = this.currentCluster?.workerOSs;
57
63
  const out = [];
@@ -240,13 +246,9 @@ export default {
240
246
  };
241
247
  }
242
248
 
243
- if (isUpgradeFromPreToStable(this.currentVersion, this.targetVersion)) {
244
- return {
245
- name: 'upgrade', tKey: 'upgrade', icon: 'icon-upgrade-alt'
246
- };
247
- }
249
+ const diff = compareChartVersions(this.currentVersion, this.targetVersion);
248
250
 
249
- if (compare(this.currentVersion, this.targetVersion) < 0) {
251
+ if (diff < 0) {
250
252
  return {
251
253
  name: 'upgrade', tKey: 'upgrade', icon: 'icon-upgrade-alt'
252
254
  };
@@ -5,6 +5,12 @@ const hasFetch = (component) => component.$options && typeof component.$options.
5
5
  export const addLifecycleHook = (vm, hook, fn) => {
6
6
  if (!vm.$options[hook]) {
7
7
  vm.$options[hook] = [];
8
+ } else if (!Array.isArray(vm.$options[hook]) && typeof vm.$options[hook] === 'function' ) {
9
+ // This caters for when....
10
+ // - component has mixins, but they have no hooks of this type (vm.$options[hook] is then not an array)
11
+ // - component has the hook (vm.$options[hook] is then a function)
12
+ // - component has both fetch and beforeMount (the component beforeMount replaces this files beforeMount with $fetch call)
13
+ vm.$options[hook] = [vm.$options[hook]];
8
14
  }
9
15
 
10
16
  if (Array.isArray(vm.$options[hook]) && !vm.$options[hook].includes(fn)) {
@@ -0,0 +1,117 @@
1
+ import AuditPolicy from '@shell/models/auditlog.cattle.io.auditpolicy';
2
+
3
+ describe('auditPolicy Model', () => {
4
+ let mockDispatch: jest.Mock;
5
+ let mockT: jest.Mock;
6
+ let auditPolicy: any;
7
+
8
+ beforeEach(() => {
9
+ mockDispatch = jest.fn();
10
+ mockT = jest.fn();
11
+
12
+ const mockResource = {
13
+ id: 'test-policy',
14
+ spec: { enabled: false },
15
+ metadata: { name: 'test-policy' }
16
+ };
17
+
18
+ auditPolicy = new AuditPolicy(mockResource, {
19
+ dispatch: mockDispatch,
20
+ rootGetters: { 'i18n/t': mockT },
21
+ getters: { schemaFor: () => ({ linkFor: jest.fn() }) }
22
+ });
23
+ });
24
+
25
+ describe('enable method', () => {
26
+ it('should call enableOrDisable with "enable"', () => {
27
+ const spy = jest.spyOn(auditPolicy, 'enableOrDisable').mockImplementation();
28
+
29
+ auditPolicy.enable();
30
+
31
+ expect(spy).toHaveBeenCalledWith('enable');
32
+ });
33
+ });
34
+
35
+ describe('disable method', () => {
36
+ it('should call enableOrDisable with "disable"', () => {
37
+ const spy = jest.spyOn(auditPolicy, 'enableOrDisable').mockImplementation();
38
+
39
+ auditPolicy.disable();
40
+
41
+ expect(spy).toHaveBeenCalledWith('disable');
42
+ });
43
+ });
44
+
45
+ describe('enableOrDisable method', () => {
46
+ let mockClone: any;
47
+
48
+ beforeEach(() => {
49
+ mockClone = {
50
+ spec: { enabled: false },
51
+ save: jest.fn()
52
+ };
53
+
54
+ mockDispatch.mockImplementation((action: string) => {
55
+ if (action === 'rancher/clone') {
56
+ return Promise.resolve(mockClone);
57
+ }
58
+
59
+ return Promise.resolve();
60
+ });
61
+ });
62
+
63
+ it('should enable policy when flag is "enable"', async() => {
64
+ mockClone.save.mockResolvedValue({});
65
+
66
+ await auditPolicy.enableOrDisable('enable');
67
+
68
+ expect(mockClone.spec.enabled).toBe(true);
69
+ expect(mockClone.save).toHaveBeenCalledWith();
70
+ });
71
+
72
+ it('should disable policy when flag is "disable"', async() => {
73
+ mockClone.save.mockResolvedValue({});
74
+
75
+ await auditPolicy.enableOrDisable('disable');
76
+
77
+ expect(mockClone.spec.enabled).toBe(false);
78
+ expect(mockClone.save).toHaveBeenCalledWith();
79
+ });
80
+
81
+ it('should handle save errors and show growl notification', async() => {
82
+ const saveError = new Error('Save failed');
83
+
84
+ mockClone.save.mockRejectedValue(saveError);
85
+ mockT.mockReturnValue('Error when enabling - test-policy');
86
+
87
+ await auditPolicy.enableOrDisable('enable');
88
+
89
+ expect(mockDispatch).toHaveBeenCalledWith('growl/fromError', {
90
+ title: 'Error when enabling - test-policy',
91
+ err: saveError,
92
+ timeout: 5000
93
+ }, { root: true });
94
+ });
95
+
96
+ it('should call translation with correct parameters', async() => {
97
+ const saveError = new Error('Save failed');
98
+
99
+ mockClone.save.mockRejectedValue(saveError);
100
+
101
+ await auditPolicy.enableOrDisable('enable');
102
+
103
+ expect(mockT).toHaveBeenCalledWith('auditPolicy.error.enableOrDisable', {
104
+ flag: 'enable',
105
+ id: 'test-policy'
106
+ });
107
+ });
108
+
109
+ it('should dispatch rancher/clone with correct parameters', async() => {
110
+ mockClone.save.mockResolvedValue({});
111
+
112
+ await auditPolicy.enableOrDisable('enable');
113
+
114
+ expect(mockDispatch).toHaveBeenCalledWith('rancher/clone', { resource: auditPolicy }, { root: true });
115
+ });
116
+ });
117
+ });