@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
@@ -112,6 +112,8 @@ interface RcItemCardProps {
112
112
 
113
113
  /** Makes the card clickable and emits 'card-click' on click/enter/space */
114
114
  clickable?: boolean;
115
+
116
+ role?: 'link' | 'button' | undefined;
115
117
  }
116
118
 
117
119
  const props = defineProps<RcItemCardProps>();
@@ -161,27 +163,23 @@ const statusTooltips = computed(() => props.header.statuses?.map((status) => lab
161
163
  const cardMeta = computed(() => ({
162
164
  ariaLabel: props.clickable ? t('itemCard.ariaLabel.clickable', { cardTitle: labelText(props.header.title) }) : undefined,
163
165
  tabIndex: props.clickable ? '0' : undefined,
164
- role: props.clickable ? 'button' : undefined,
165
- actionMenuLabel: props.actions && t('itemCard.actionMenu.label', { cardTitle: labelText(props.header.title) })
166
+ role: props.role ?? (props.clickable ? 'button' : undefined),
167
+ actionMenuLabel: props.actions && t('itemCard.actionMenu.label', { cardTitle: labelText(props.header.title) }),
166
168
  }));
167
169
 
170
+ const cursorValue = computed(() => props.clickable ? 'pointer' : 'auto');
168
171
  </script>
169
172
 
170
173
  <template>
171
174
  <div
172
175
  ref="cardEl"
173
176
  class="item-card"
177
+ :data-testid="`item-card-${id}`"
174
178
  :class="{
175
179
  'clickable':
176
180
  clickable
177
181
  }"
178
- :role="cardMeta.role"
179
- :tabindex="cardMeta.tabIndex"
180
- :aria-label="cardMeta.ariaLabel"
181
- :data-testid="`item-card-${id}`"
182
182
  @click="_handleCardClick"
183
- @keydown.enter="_handleCardClick"
184
- @keydown.space.prevent="_handleCardClick"
185
183
  >
186
184
  <div :class="['item-card-body', variant]">
187
185
  <template v-if="variant !== 'small'">
@@ -214,7 +212,16 @@ const cardMeta = computed(() => ({
214
212
 
215
213
  <div :class="['item-card-body-details', variant]">
216
214
  <div :class="['item-card-header', variant]">
217
- <div class="item-card-header-left">
215
+ <div
216
+ class="item-card-header-left"
217
+ :data-testid="`card-header-left`"
218
+ :role="cardMeta.role"
219
+ :tabindex="cardMeta.tabIndex"
220
+ :aria-label="cardMeta.ariaLabel"
221
+ @click.self="_handleCardClick"
222
+ @keydown.enter="_handleCardClick"
223
+ @keydown.space.prevent="_handleCardClick"
224
+ >
218
225
  <template v-if="variant === 'small'">
219
226
  <slot name="item-card-image">
220
227
  <div
@@ -315,16 +322,22 @@ $image-medium-box-width: 48px;
315
322
  border-radius: var(--border-radius-md);
316
323
  border: 1px solid var(--border);
317
324
  background: var(--body-bg);
325
+ cursor: v-bind(cursorValue);
318
326
 
319
327
  &.clickable:hover {
320
328
  border-color: var(--primary);
321
329
  }
322
330
 
323
- &:focus-visible {
331
+ &:has(.item-card-header-left:focus-visible) {
332
+ border-color: var(--primary);
324
333
  @include focus-outline;
325
334
  outline-offset: -2px;
326
335
  }
327
336
 
337
+ &:focus-visible {
338
+ outline: none;
339
+ }
340
+
328
341
  &-image {
329
342
  width: $image-medium-box-width;
330
343
  height: $image-medium-box-width;
@@ -358,6 +371,10 @@ $image-medium-box-width: 48px;
358
371
  &-left {
359
372
  flex-grow: 1;
360
373
  min-width: 0;
374
+
375
+ &:focus-visible {
376
+ outline: none;
377
+ }
361
378
  }
362
379
 
363
380
  &-title {
package/store/auth.js CHANGED
@@ -1,6 +1,5 @@
1
1
  import { GITHUB_NONCE, GITHUB_REDIRECT, GITHUB_SCOPE } from '@shell/config/query-params';
2
- import { NORMAN } from '@shell/config/types';
3
- import { _MULTI } from '@shell/plugins/dashboard-store/actions';
2
+ import { MANAGEMENT, EXT } from '@shell/config/types';
4
3
  import { addObjects, findBy, joinStringList } from '@shell/utils/array';
5
4
  import { openAuthPopup, returnTo } from '@shell/utils/auth';
6
5
  import { base64Encode } from '@shell/utils/crypto';
@@ -41,8 +40,9 @@ export const state = function() {
41
40
  hasAuth: null,
42
41
  loggedIn: false,
43
42
  principalId: null,
44
- v3User: null,
43
+ user: null,
45
44
  initialPass: null,
45
+ selfUser: null,
46
46
  };
47
47
  };
48
48
 
@@ -63,8 +63,8 @@ export const getters = {
63
63
  return state.principalId;
64
64
  },
65
65
 
66
- v3User(state) {
67
- return state.v3User;
66
+ user(state) {
67
+ return state.user;
68
68
  },
69
69
 
70
70
  initialPass(state) {
@@ -73,6 +73,10 @@ export const getters = {
73
73
 
74
74
  isGithub(state) {
75
75
  return state.principalId && state.principalId.startsWith('github_user://');
76
+ },
77
+
78
+ selfUser(state) {
79
+ return state.selfUser;
76
80
  }
77
81
  };
78
82
 
@@ -81,9 +85,13 @@ export const mutations = {
81
85
  state.fromHeader = fromHeader;
82
86
  },
83
87
 
84
- gotUser(state, v3User) {
88
+ gotUser(state, user) {
85
89
  // Always deference to avoid race condition when setting `mustChangePassword`
86
- state.v3User = { ...v3User };
90
+ state.user = { ...user };
91
+ },
92
+
93
+ gotSelfUser(state, selfUser) {
94
+ state.selfUser = selfUser;
87
95
  },
88
96
 
89
97
  hasAuth(state, hasAuth) {
@@ -101,7 +109,8 @@ export const mutations = {
101
109
 
102
110
  state.loggedIn = false;
103
111
  state.principalId = null;
104
- state.v3User = null;
112
+ state.user = null;
113
+ state.selfUser = null;
105
114
  state.initialPass = null;
106
115
  },
107
116
 
@@ -115,22 +124,42 @@ export const actions = {
115
124
  commit('gotHeader', fromHeader);
116
125
  },
117
126
 
127
+ async updateSelfUser({ dispatch, commit }, selfUser) {
128
+ const classifiedSelfUser = await dispatch('management/create', selfUser, { root: true });
129
+
130
+ commit('gotSelfUser', classifiedSelfUser);
131
+ },
132
+
133
+ async getSelfUser({ commit, dispatch, getters }) {
134
+ if (getters.selfUser) {
135
+ return Promise.resolve(getters.selfUser);
136
+ }
137
+
138
+ const selfUser = await dispatch('management/request', {
139
+ url: `/v1/${ EXT.SELFUSER }`,
140
+ method: 'POST',
141
+ data: {},
142
+ }, { root: true });
143
+
144
+ await dispatch('updateSelfUser', selfUser);
145
+ },
146
+
118
147
  async getUser({ dispatch, commit, getters }) {
119
- if (getters.v3User) {
148
+ if (getters.user) {
120
149
  return;
121
150
  }
122
151
 
123
152
  try {
124
- const user = await dispatch('rancher/findAll', {
125
- type: NORMAN.USER,
126
- opt: {
127
- url: '/v3/users',
128
- filter: { me: true },
129
- load: _MULTI
130
- }
131
- }, { root: true });
153
+ let mgmtUser;
132
154
 
133
- commit('gotUser', user?.[0]);
155
+ await dispatch('getSelfUser');
156
+ const selfUser = getters.selfUser;
157
+
158
+ if (selfUser) {
159
+ mgmtUser = await dispatch('management/request', { url: `/v1/${ MANAGEMENT.USER }/${ selfUser.status?.userID }` }, { root: true });
160
+ }
161
+
162
+ commit('gotUser', mgmtUser);
134
163
  } catch { }
135
164
  },
136
165
 
@@ -266,7 +295,16 @@ export const actions = {
266
295
  }
267
296
 
268
297
  if (driver?.scopes) {
269
- scopes = [joinStringList(scopes[0], driver.scopes)];
298
+ // In some cases, driver scopes can be an array. We need to convert this
299
+ // to a string that can be parsed by `joinStringList()`
300
+ try {
301
+ const driverScopes = Array.isArray(driver.scopes) ? driver.scopes.join(' ') : driver.scopes;
302
+
303
+ scopes = [joinStringList(scopes[0], driverScopes)];
304
+ } catch (error) {
305
+ // eslint-disable-next-line no-console
306
+ console.error('Failed to join driver scopes', error);
307
+ }
270
308
  }
271
309
 
272
310
  let url = removeParam(redirectUrl, GITHUB_SCOPE);
@@ -387,7 +387,7 @@ export const actions = {
387
387
  */
388
388
  async init({ commit, getters } : any, userData: any) {
389
389
  const userKey = userData.id;
390
- const userId = userData.v3User?.uuid;
390
+ const userId = userData.user?.metadata?.uid;
391
391
 
392
392
  if (!userKey || !userId) {
393
393
  console.error('Unable to initialize notifications - required user info not available'); // eslint-disable-line no-console
package/store/type-map.js CHANGED
@@ -189,7 +189,7 @@ export const TYPE_MODES = {
189
189
  */
190
190
  FAVORITE: 'favorite',
191
191
  /**
192
- * Represents no virtual or spoofed types that have a count.
192
+ * Represents types that have a count and are not virtual or spoofed.
193
193
  *
194
194
  * For example the `More Resource` in the cluster explorer
195
195
  *
@@ -1586,6 +1586,17 @@ export const mutations = {
1586
1586
  collection: `/${ SPOOFED_PREFIX }/${ schema.id }`,
1587
1587
  ...(schema.links || {})
1588
1588
  };
1589
+
1590
+ const verbs = schema.attributes?.verbs || [];
1591
+
1592
+ if ( !verbs.includes('list') ) {
1593
+ verbs.push('list');
1594
+ }
1595
+
1596
+ schema.attributes = {
1597
+ ...schema?.attributes,
1598
+ verbs
1599
+ };
1589
1600
  });
1590
1601
 
1591
1602
  const existing = findBy(state.spoofedTypes[product], 'type', copy.type);
@@ -2077,6 +2077,7 @@ export const POD_DISRUPTION_BUDGET: "policy.poddisruptionbudget";
2077
2077
  export const PV: "persistentvolume";
2078
2078
  export const PVC: "persistentvolumeclaim";
2079
2079
  export const RESOURCE_QUOTA: "resourcequota";
2080
+ export const AUDIT_POLICY: "auditlog.cattle.io.auditpolicy";
2080
2081
  export const SCHEMA: "schema";
2081
2082
  export const SERVICE: "service";
2082
2083
  export const SECRET: "secret";
@@ -2246,6 +2247,9 @@ export namespace BRAND {
2246
2247
  }
2247
2248
  export namespace EXT {
2248
2249
  let USER_ACTIVITY: string;
2250
+ let SELFUSER: string;
2251
+ let GROUP_MEMBERSHIP_REFRESH_REQUESTS: string;
2252
+ let PASSWORD_CHANGE_REQUESTS: string;
2249
2253
  let KUBECONFIG: string;
2250
2254
  }
2251
2255
  export namespace CAPI {
@@ -2378,6 +2382,9 @@ export const DEPRECATED: "Deprecated";
2378
2382
  export const EXPERIMENTAL: "Experimental";
2379
2383
  export const AUTOSCALER_CONFIG_MAP_ID: "kube-system/cluster-autoscaler-status";
2380
2384
  export const HOSTED_PROVIDER: "hostedprovider";
2385
+ export namespace SAVED_COUNTS {
2386
+ let K8S_CLUSTERS: string;
2387
+ }
2381
2388
  }
2382
2389
 
2383
2390
  // @shell/config/version
@@ -3732,20 +3739,8 @@ export default class Resource {
3732
3739
  rows: any[];
3733
3740
  };
3734
3741
  };
3735
- get _cards(): {
3736
- component: any;
3737
- props: {
3738
- title: any;
3739
- rows: any[];
3740
- };
3741
- }[];
3742
- get cards(): {
3743
- component: any;
3744
- props: {
3745
- title: any;
3746
- rows: any[];
3747
- };
3748
- }[];
3742
+ get _cards(): any[];
3743
+ get cards(): any[];
3749
3744
  }
3750
3745
  }
3751
3746
 
@@ -3804,6 +3799,7 @@ export default class SteveModel extends HybridModel {
3804
3799
  get modelExtensions(): any;
3805
3800
  cleanForSave(data: any, forNew: any): any;
3806
3801
  paginationEnabled(): any;
3802
+ processSaveResponse(res: any): void;
3807
3803
  }
3808
3804
  import HybridModel from './hybrid-class';
3809
3805
  }
@@ -4089,6 +4085,20 @@ export function overlayIndividualBanners(parsedBanner: any, banners: any): void;
4089
4085
  // @shell/utils/chart
4090
4086
 
4091
4087
  declare module '@shell/utils/chart' {
4088
+ /**
4089
+ * Compares two chart versions using SemVer logic, with special handling for Rancher's "up" build metadata.
4090
+ *
4091
+ * It uses `semver.compare` for the primary comparison. If versions are considered equal (SemVer ignores build metadata),
4092
+ * it checks if both versions have build metadata starting with "up". If so, it strips the "up" prefix and compares the remaining strings as versions.
4093
+ *
4094
+ * If the "up" logic doesn't apply or results in equality, it falls back to `semver.compareBuild` to handle
4095
+ * other build metadata differences (e.g. sorting alphabetically).
4096
+ *
4097
+ * @param {string} v1 - The first version string.
4098
+ * @param {string} v2 - The second version string.
4099
+ * @returns {number} - 0 if equal, -1 if v1 < v2, 1 if v1 > v2.
4100
+ */
4101
+ export function compareChartVersions(v1: string, v2: string): number;
4092
4102
  /**
4093
4103
  * Get the latest chart version that is compatible with the cluster's OS and user's pre-release preference.
4094
4104
  * @param {Object} chart - The chart object.
@@ -5408,7 +5418,6 @@ export function parse(str: any): any;
5408
5418
  export function sortable(str: any): any;
5409
5419
  export function compare(in1: any, in2: any): any;
5410
5420
  export function isPrerelease(version?: string): boolean;
5411
- export function isUpgradeFromPreToStable(currentVersion: any, targetVersion: any): any;
5412
5421
  export function isDevBuild(version: any): boolean;
5413
5422
  export function getVersionInfo(store: any): {
5414
5423
  displayVersion: any;
@@ -46,6 +46,10 @@ export interface ActionFindAllArgs extends ActionCoreFindArgs {
46
46
  * This is done via the native kube pagination api, not steve
47
47
  */
48
48
  depaginate?: boolean,
49
+ /**
50
+ * Specifies the name to use if we should save the count returned in the paginated request
51
+ */
52
+ saveCountAs?: string,
49
53
  }
50
54
 
51
55
  /**
@@ -76,6 +80,9 @@ export interface ActionFindPageArgs extends ActionCoreFindArgs {
76
80
  * If true don't persist the http response to the store, just pass it back
77
81
  */
78
82
  transient?: boolean,
83
+
84
+ saveCountAs?: string,
85
+
79
86
  /**
80
87
  * The target minimum revision for the resource.
81
88
  *
@@ -0,0 +1,96 @@
1
+ import { compareChartVersions } from '@shell/utils/chart';
2
+
3
+ describe('compareChartVersions', () => {
4
+ describe('standard SemVer Comparison', () => {
5
+ it('should correctly compare standard versions', () => {
6
+ expect(compareChartVersions('1.0.0', '2.0.0')).toBe(-1);
7
+ expect(compareChartVersions('2.0.0', '1.0.0')).toBe(1);
8
+ expect(compareChartVersions('1.0.0', '1.0.0')).toBe(0);
9
+ });
10
+
11
+ it('should compare minor and patch versions correctly', () => {
12
+ expect(compareChartVersions('1.0.0', '1.1.0')).toBe(-1);
13
+ expect(compareChartVersions('1.0.0', '1.0.1')).toBe(-1);
14
+ expect(compareChartVersions('1.1.0', '1.0.1')).toBe(1);
15
+ });
16
+
17
+ it('should handle loose parsing (v-prefix)', () => {
18
+ expect(compareChartVersions('v1.0.0', '1.0.0')).toBe(0);
19
+ expect(compareChartVersions('v1.0.0', 'v2.0.0')).toBe(-1);
20
+ });
21
+ });
22
+
23
+ describe('rancher "up" Build Metadata Logic', () => {
24
+ it('should compare inner versions when both have "up" prefix', () => {
25
+ // 1.0.0 vs 2.0.0 inside the metadata
26
+ expect(compareChartVersions('1.0.0+up1.0.0', '1.0.0+up2.0.0')).toBe(-1);
27
+ expect(compareChartVersions('1.0.0+up2.0.0', '1.0.0+up1.0.0')).toBe(1);
28
+ // Equal inner versions
29
+ expect(compareChartVersions('1.0.0+up1.0.0', '1.0.0+up1.0.0')).toBe(0);
30
+ });
31
+
32
+ it('should handle pre-releases within "up" metadata correctly', () => {
33
+ // Crucial test: semver logic ensures 1.0.0-rc.1 < 1.0.0
34
+ // Standard string sort would often fail here depending on the string
35
+ expect(compareChartVersions('1.0.0+up1.0.0-rc.1', '1.0.0+up1.0.0')).toBe(-1);
36
+ expect(compareChartVersions('1.0.0+up1.0.0', '1.0.0+up1.0.0-rc.1')).toBe(1);
37
+ });
38
+
39
+ it('should compare different inner major/minor/patch versions', () => {
40
+ expect(compareChartVersions('0.0.1+up1.0.0', '0.0.1+up0.1.0')).toBe(1);
41
+ expect(compareChartVersions('0.0.1+up0.1.0', '0.0.1+up1.0.0')).toBe(-1);
42
+ });
43
+
44
+ it('should prioritize valid inner semver over invalid inner semver', () => {
45
+ // Valid "up" version > Invalid "up" version
46
+ expect(compareChartVersions('1.0.0+up1.0.0', '1.0.0+upInvalid')).toBe(1);
47
+ expect(compareChartVersions('1.0.0+upInvalid', '1.0.0+up1.0.0')).toBe(-1);
48
+ });
49
+
50
+ it('should fall back to lexical sort if both "up" suffixes are invalid semver', () => {
51
+ // Both are "up..." but not valid semver, so it falls back to semver.compareBuild (lexical)
52
+ expect(compareChartVersions('1.0.0+upA', '1.0.0+upB')).toBe(-1);
53
+ expect(compareChartVersions('1.0.0+upB', '1.0.0+upA')).toBe(1);
54
+ });
55
+ });
56
+
57
+ describe('standard Build Metadata Fallback', () => {
58
+ it('should correctly compare versions with standard build metadata (lexicographical)', () => {
59
+ // 1.0.0+a vs 1.0.0+b -> -1
60
+ expect(compareChartVersions('1.0.0+a', '1.0.0+b')).toBe(-1);
61
+ expect(compareChartVersions('1.0.0+b', '1.0.0+a')).toBe(1);
62
+ // 1.0.0+1 vs 1.0.0+2 -> -1
63
+ expect(compareChartVersions('1.0.0+1', '1.0.0+2')).toBe(-1);
64
+ });
65
+
66
+ it('should use standard comparison if only one has "up" prefix', () => {
67
+ // "up" comes after "foo" lexically
68
+ expect(compareChartVersions('1.0.0+foo', '1.0.0+up1.0.0')).toBe(-1);
69
+ expect(compareChartVersions('1.0.0+up1.0.0', '1.0.0+foo')).toBe(1);
70
+ });
71
+ });
72
+
73
+ describe('edge Cases and Invalid Inputs', () => {
74
+ it('should handle null or undefined inputs safely', () => {
75
+ // Implementation behavior: fallback to utils/version compare
76
+ // which treats falsy as "high" (return 1) if first arg is null?
77
+ // Checking implementation of `compare` in shell/utils/version.js:
78
+ // if (!in1) return 1; if (!in2) return -1;
79
+ expect(compareChartVersions(null, '1.0.0')).toBe(1);
80
+ expect(compareChartVersions('1.0.0', null)).toBe(-1);
81
+ expect(compareChartVersions(undefined, '1.0.0')).toBe(1);
82
+ expect(compareChartVersions('1.0.0', undefined)).toBe(-1);
83
+ expect(compareChartVersions(null, null)).toBe(1); // First check is !in1 -> 1
84
+ });
85
+
86
+ it('should handle completely invalid strings', () => {
87
+ // "invalid" is not valid semver, so it falls back to utils/version compare (string/numeric comparison)
88
+ // "invalid" vs "1.0.0"
89
+ // "invalid" is treated as string, "1.0.0" parsed as parts
90
+ // Effectively tests the fallback logic stability
91
+ expect(compareChartVersions('invalid', '1.0.0')).not.toBe(0);
92
+ expect(compareChartVersions('a', 'b')).toBe(-1);
93
+ expect(compareChartVersions('b', 'a')).toBe(1);
94
+ });
95
+ });
96
+ });
@@ -1,4 +1,4 @@
1
- import { isDevBuild, isUpgradeFromPreToStable, getReleaseNotesURL } from '@shell/utils/version';
1
+ import { isDevBuild, getReleaseNotesURL } from '@shell/utils/version';
2
2
 
3
3
  describe('fx: isDevBuild', () => {
4
4
  it.each([
@@ -17,24 +17,6 @@ describe('fx: isDevBuild', () => {
17
17
  );
18
18
  });
19
19
 
20
- describe('fx: isUpgradeFromPreToStable', () => {
21
- it('should be true when going from pre-release to stable of same version', () => {
22
- expect(isUpgradeFromPreToStable('1.0.0-rc1', '1.0.0')).toBe(true);
23
- });
24
-
25
- it('should be false when going from stable to pre-release', () => {
26
- expect(isUpgradeFromPreToStable('1.0.0', '1.0.0-rc1')).toBe(false );
27
- });
28
-
29
- it('should be false for stable to stable', () => {
30
- expect(isUpgradeFromPreToStable('1.0.0', '1.1.0')).toBe(false);
31
- });
32
-
33
- it('should be false for pre-release to pre-release', () => {
34
- expect(isUpgradeFromPreToStable('1.0.0-rc1', '1.0.0-rc2')).toBe(false);
35
- });
36
- });
37
-
38
20
  describe('fx: getReleaseNotesURL', () => {
39
21
  describe('when version is not provided', () => {
40
22
  it('should return the community dev URL', () => {
package/utils/chart.js CHANGED
@@ -1,5 +1,69 @@
1
+ import semver from 'semver';
2
+ import { compare } from '@shell/utils/version';
1
3
  import { compatibleVersionsFor } from '@shell/store/catalog';
2
4
 
5
+ /**
6
+ * Compares two chart versions using SemVer logic, with special handling for Rancher's "up" build metadata.
7
+ *
8
+ * It uses `semver.compare` for the primary comparison. If versions are considered equal (SemVer ignores build metadata),
9
+ * it checks if both versions have build metadata starting with "up". If so, it strips the "up" prefix and compares the remaining strings as versions.
10
+ *
11
+ * If the "up" logic doesn't apply or results in equality, it falls back to `semver.compareBuild` to handle
12
+ * other build metadata differences (e.g. sorting alphabetically).
13
+ *
14
+ * @param {string} v1 - The first version string.
15
+ * @param {string} v2 - The second version string.
16
+ * @returns {number} - 0 if equal, -1 if v1 < v2, 1 if v1 > v2.
17
+ */
18
+ export function compareChartVersions(v1, v2) {
19
+ const v1Valid = semver.valid(v1, { loose: true });
20
+ const v2Valid = semver.valid(v2, { loose: true });
21
+
22
+ if (!v1Valid || !v2Valid) {
23
+ return compare(v1, v2);
24
+ }
25
+
26
+ // semver.compare ignores build metadata (e.g., 1.0.0+1 == 1.0.0+2)
27
+ let diff = semver.compare(v1, v2, { loose: true });
28
+
29
+ if (diff === 0) {
30
+ const parsedV1 = semver.parse(v1, { loose: true });
31
+ const parsedV2 = semver.parse(v2, { loose: true });
32
+ const buildV1 = parsedV1.build.join('.');
33
+ const buildV2 = parsedV2.build.join('.');
34
+
35
+ // Special logic for Rancher charts where "up" prefix in build metadata contains version info.
36
+ // E.g. 108.0.0+up0.25.0-rc.4 vs 108.0.0+up0.25.0
37
+ // Standard semver.compareBuild would sort ASCII: "up...-rc" > "up..." (incorrect for RC)
38
+ // We strip "up" and compare the rest as versions to properly handle pre-releases (RC < Stable).
39
+ if (buildV1.startsWith('up') && buildV2.startsWith('up')) {
40
+ const subV1 = buildV1.substring(2);
41
+ const subV2 = buildV2.substring(2);
42
+ const subV1Valid = semver.valid(subV1, { loose: true });
43
+ const subV2Valid = semver.valid(subV2, { loose: true });
44
+
45
+ if (subV1Valid && subV2Valid) {
46
+ // Both "up" metadata parts are valid semver: compare them semantically.
47
+ diff = semver.compare(subV1, subV2, { loose: true });
48
+ } else if (subV1Valid && !subV2Valid) {
49
+ // Only v1 has valid "up" metadata: prefer v1 over v2.
50
+ diff = 1;
51
+ } else if (!subV1Valid && subV2Valid) {
52
+ // Only v2 has valid "up" metadata: prefer v2 over v1.
53
+ diff = -1;
54
+ }
55
+ }
56
+
57
+ // Fallback to standard build comparison for other cases (e.g. 1.0.0+1 vs 1.0.0+2).
58
+ // semver.compareBuild sorts build metadata lexicographically.
59
+ if (diff === 0) {
60
+ diff = semver.compareBuild(v1, v2, { loose: true });
61
+ }
62
+ }
63
+
64
+ return diff;
65
+ }
66
+
3
67
  /**
4
68
  * Get the latest chart version that is compatible with the cluster's OS and user's pre-release preference.
5
69
  * @param {Object} chart - The chart object.
@@ -31,6 +31,11 @@ interface Args {
31
31
  classify?: boolean,
32
32
  reactive?: boolean,
33
33
  }
34
+
35
+ /**
36
+ * Specifies the name to use if we should save the count returned in the paginated request
37
+ */
38
+ saveCountAs?: string;
34
39
  }
35
40
 
36
41
  interface Result<T> extends Omit<ActionFindPageTransientResponse<T>, 'data'> {
@@ -57,6 +62,7 @@ class PaginationWrapper<T extends object> {
57
62
  private backOffId: string;
58
63
  private classify: boolean;
59
64
  private reactive: boolean;
65
+ private saveCountAs: string | undefined;
60
66
  private cachedRevision?: string;
61
67
  private cachedResult?: Result<T>;
62
68
 
@@ -65,7 +71,7 @@ class PaginationWrapper<T extends object> {
65
71
 
66
72
  constructor(args: Args) {
67
73
  const {
68
- $store, id, enabledFor, onChange, formatResponse
74
+ $store, id, enabledFor, onChange, formatResponse, saveCountAs
69
75
  } = args;
70
76
 
71
77
  this.$store = $store;
@@ -75,6 +81,7 @@ class PaginationWrapper<T extends object> {
75
81
  this.onChange = onChange;
76
82
  this.classify = formatResponse?.classify || false;
77
83
  this.reactive = formatResponse?.reactive || false;
84
+ this.saveCountAs = saveCountAs;
78
85
 
79
86
  this.isEnabled = paginationUtils.isEnabled({ rootGetters: $store.getters, $extension: this.$store.$extension }, enabledFor);
80
87
  }
@@ -152,9 +159,10 @@ class PaginationWrapper<T extends object> {
152
159
  },
153
160
  delayedFn: async() => {
154
161
  const opt: ActionFindPageArgs = {
155
- watch: false,
162
+ watch: false,
156
163
  pagination,
157
- transient: true,
164
+ transient: true,
165
+ saveCountAs: this.saveCountAs,
158
166
  revision
159
167
  };
160
168
  const res: ActionFindPageTransientResponse<T> = await this.$store.dispatch(`${ this.enabledFor.store }/findPage`, { opt, type });
package/utils/version.js CHANGED
@@ -3,6 +3,7 @@ import semver from 'semver';
3
3
  import { MANAGEMENT } from '@shell/config/types';
4
4
  import { READ_WHATS_NEW, SEEN_WHATS_NEW } from '@shell/store/prefs';
5
5
  import { SETTING } from '@shell/config/settings';
6
+ import { getVersionData } from '@shell/config/version';
6
7
 
7
8
  export function parse(str) {
8
9
  str = `${ str }`;
@@ -74,21 +75,6 @@ export function isPrerelease(version = '') {
74
75
  return !!semver.prerelease(version);
75
76
  }
76
77
 
77
- export function isUpgradeFromPreToStable(currentVersion, targetVersion) {
78
- if (!isPrerelease(currentVersion) || isPrerelease(targetVersion)) {
79
- return false;
80
- }
81
-
82
- const cVersion = semver.clean(currentVersion, { loose: true });
83
- const tVersion = semver.clean(targetVersion, { loose: true });
84
-
85
- if (cVersion && tVersion && semver.valid(cVersion) && semver.valid(tVersion)) {
86
- return semver.lt(cVersion, tVersion);
87
- }
88
-
89
- return false;
90
- }
91
-
92
78
  export function isDevBuild(version) {
93
79
  if ( ['dev', 'master', 'head'].includes(version) || version.endsWith('-head') || version.match(/-rc\d+$/) || version.match(/-alpha\d+$/) ) {
94
80
  return true;
@@ -98,8 +84,10 @@ export function isDevBuild(version) {
98
84
  }
99
85
 
100
86
  export function getVersionInfo(store) {
101
- const setting = store.getters['management/byId'](MANAGEMENT.SETTING, SETTING.VERSION_RANCHER);
102
- const fullVersion = setting?.value || 'unknown';
87
+ const fullVersion = store.getters['management/byId'](MANAGEMENT.SETTING, SETTING.VERSION_RANCHER)?.value ??
88
+ getVersionData()?.Version ??
89
+ 'unknown';
90
+
103
91
  let displayVersion = fullVersion;
104
92
 
105
93
  const match = fullVersion.match(/^(.*)-([0-9a-f]{40})-(.*)$/);