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

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.
@@ -0,0 +1,97 @@
1
+ import { CAPI, MANAGEMENT } from '@shell/config/types';
2
+ import SteveModel from '@shell/plugins/steve/steve-class';
3
+ import { Location } from 'vue-router';
4
+
5
+ interface ReferencedCluster {
6
+ label: string;
7
+ location: Location | null;
8
+ }
9
+
10
+ export default class Kubeconfig extends SteveModel {
11
+ declare spec: {
12
+ clusters?: string[];
13
+ ttl?: number;
14
+ };
15
+
16
+ declare metadata: {
17
+ name?: string;
18
+ creationTimestamp?: string;
19
+ };
20
+
21
+ get _availableActions(): object[] {
22
+ const out = super._availableActions;
23
+
24
+ // Remove element at index 1 (the first divider), the actions that don't make sense.
25
+ return out.filter((action: { action: string }, index: number) => index !== 1 && !['goToEdit', 'goToEditYaml', 'cloneYaml', 'download'].includes(action.action));
26
+ }
27
+
28
+ /**
29
+ * Calculates the expiry timestamp from creationTimestamp + ttl.
30
+ * Returns an ISO date string for use with LiveDate formatter.
31
+ */
32
+ get expiresAt(): string | null {
33
+ const ttlSeconds = this.spec?.ttl;
34
+ const creationTimestamp = this.metadata?.creationTimestamp;
35
+
36
+ if (!ttlSeconds || !creationTimestamp) {
37
+ return null;
38
+ }
39
+
40
+ const createdAt = new Date(creationTimestamp);
41
+ const expiresAt = new Date(createdAt.getTime() + (ttlSeconds * 1000));
42
+
43
+ return expiresAt.toISOString();
44
+ }
45
+
46
+ /**
47
+ * Returns cluster information for display and linking.
48
+ * Each object contains {label, location} where location is null if cluster doesn't exist.
49
+ */
50
+ get referencedClusters(): ReferencedCluster[] {
51
+ const clusterIds = this.spec?.clusters || [];
52
+ const provClusters = this.$rootGetters['management/all'](CAPI.RANCHER_CLUSTER) || [];
53
+ const mgmtClusters = this.$rootGetters['management/all'](MANAGEMENT.CLUSTER) || [];
54
+
55
+ return clusterIds.map((id: string) => {
56
+ const provCluster = provClusters.find((c: any) => c.mgmt?.id === id || c.status?.clusterName === id);
57
+ const mgmtCluster = mgmtClusters.find((c: any) => c.id === id);
58
+ const cluster = provCluster || mgmtCluster;
59
+
60
+ return {
61
+ label: cluster?.nameDisplay || this.t('"ext.cattle.io.kubeconfig".deleted', { name: id }),
62
+ location: provCluster?.detailLocation || mgmtCluster?.detailLocation || null
63
+ };
64
+ });
65
+ }
66
+
67
+ /**
68
+ * Returns referenced clusters sorted: existing clusters first (by name), then deleted clusters.
69
+ */
70
+ get sortedReferencedClusters(): ReferencedCluster[] {
71
+ return this.referencedClusters.slice().sort((a, b) => {
72
+ const aExists = a.location !== null;
73
+ const bExists = b.location !== null;
74
+
75
+ if (aExists && !bExists) {
76
+ return -1;
77
+ }
78
+ if (!aExists && bExists) {
79
+ return 1;
80
+ }
81
+
82
+ const aName = a.label.toLowerCase();
83
+ const bName = b.label.toLowerCase();
84
+
85
+ return aName.localeCompare(bName, undefined, { numeric: true });
86
+ });
87
+ }
88
+
89
+ /**
90
+ * Returns a sortable string for the clusters column.
91
+ */
92
+ get referencedClustersSortable(): string {
93
+ return this.sortedReferencedClusters
94
+ .map((c) => c.label.toLowerCase())
95
+ .join(',');
96
+ }
97
+ }
package/models/secret.js CHANGED
@@ -573,7 +573,7 @@ export default class Secret extends SteveModel {
573
573
  const id = this.id?.replace(/.*\//, '');
574
574
 
575
575
  return {
576
- name: `c-cluster-product-resource-namespace-id`,
576
+ name: `c-cluster-product-${ VIRTUAL_TYPES.PROJECT_SECRETS }-namespace-id`,
577
577
  params: {
578
578
  product: this.$rootGetters['productId'],
579
579
  cluster: this.$rootGetters['clusterId'],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rancher/shell",
3
- "version": "3.0.9-rc.3",
3
+ "version": "3.0.9-rc.4",
4
4
  "description": "Rancher Dashboard Shell",
5
5
  "repository": "https://github.com/rancher/dashboard",
6
6
  "license": "Apache-2.0",
@@ -39,7 +39,7 @@
39
39
  "@babel/preset-typescript": "7.16.7",
40
40
  "@novnc/novnc": "1.2.0",
41
41
  "@popperjs/core": "2.11.8",
42
- "@rancher/icons": "2.0.54",
42
+ "@rancher/icons": "2.0.55",
43
43
  "@types/is-url": "1.2.30",
44
44
  "@types/node": "20.10.8",
45
45
  "@types/semver": "^7.5.8",
package/pages/about.vue CHANGED
@@ -9,6 +9,7 @@ import { mapGetters } from 'vuex';
9
9
  import TabTitle from '@shell/components/TabTitle';
10
10
  import { PanelLocation, ExtensionPoint } from '@shell/core/types';
11
11
  import ExtensionPanel from '@shell/components/ExtensionPanel';
12
+ import { getVersionInfo } from '@shell/utils/version';
12
13
 
13
14
  export default {
14
15
  components: {
@@ -30,7 +31,7 @@ export default {
30
31
  computed: {
31
32
  ...mapGetters(['releaseNotesUrl']),
32
33
  rancherVersion() {
33
- return this.settings.find((s) => s.id === SETTING.VERSION_RANCHER);
34
+ return getVersionInfo(this.$store).fullVersion;
34
35
  },
35
36
  appName() {
36
37
  return getVendor();
@@ -124,7 +125,7 @@ export default {
124
125
  >
125
126
  {{ t("about.versions.rancher") }}
126
127
  </a>
127
- </td><td>{{ rancherVersion.value }}</td>
128
+ </td><td>{{ rancherVersion }}</td>
128
129
  </tr>
129
130
  <tr v-if="dashboardVersion">
130
131
  <td>
@@ -1,5 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import { RcItemCardAction } from '@components/RcItemCard';
3
+ import { RcButton } from '@components/RcButton';
3
4
 
4
5
  interface FooterItem {
5
6
  icon?: string;
@@ -30,26 +31,34 @@ function onClickItem(type: string, label: string) {
30
31
  class="app-chart-card-footer-item"
31
32
  data-testid="app-chart-card-footer-item"
32
33
  >
33
- <i
34
- v-if="footerItem.icon"
35
- v-clean-tooltip="t(footerItem.iconTooltip?.key)"
36
- :class="['icon', 'app-chart-card-footer-item-icon', footerItem.icon]"
37
- />
38
34
  <template
39
35
  v-for="(label, j) in footerItem.labels"
40
36
  :key="j"
41
37
  >
42
38
  <rc-item-card-action
43
39
  v-if="clickable && footerItem.type"
44
- v-clean-tooltip="footerItem.labelTooltip"
45
- class="app-chart-card-footer-item-text secondary-text-link"
46
- data-testid="app-chart-card-footer-item-text"
47
- tabindex="0"
48
- :aria-label="t('catalog.charts.appChartCard.footerItem.ariaLabel')"
49
- @click="onClickItem(footerItem.type, label)"
40
+ class="app-chart-card-footer-item-text"
50
41
  >
51
- {{ label }}
52
- <span v-if="footerItem.labels.length > 1 && j !== footerItem.labels.length - 1">, </span>
42
+ <rc-button
43
+ v-clean-tooltip="footerItem.labelTooltip"
44
+ variant="ghost"
45
+ class="app-chart-card-footer-button secondary-text-link"
46
+ data-testid="app-chart-card-footer-item-text"
47
+ :aria-label="t('catalog.charts.appChartCard.footerItem.ariaLabel', { filter: label })"
48
+ @click="onClickItem(footerItem.type, label)"
49
+ >
50
+ <template
51
+ v-if="footerItem.icon"
52
+ #before
53
+ >
54
+ <i
55
+ v-clean-tooltip="t(footerItem.iconTooltip?.key)"
56
+ :class="['icon', 'app-chart-card-footer-item-icon', footerItem.icon]"
57
+ />
58
+ </template>
59
+ {{ label }}
60
+ <span v-if="footerItem.labels.length > 1 && j !== footerItem.labels.length - 1">, </span>
61
+ </rc-button>
53
62
  </rc-item-card-action>
54
63
  <span
55
64
  v-else
@@ -78,7 +87,6 @@ function onClickItem(type: string, label: string) {
78
87
  margin-right: 8px;
79
88
 
80
89
  &-text {
81
- text-transform: capitalize;
82
90
  margin-right: 8px;
83
91
  display: -webkit-box;
84
92
  -webkit-line-clamp: 1;
@@ -98,5 +106,21 @@ function onClickItem(type: string, label: string) {
98
106
  margin-right: 8px;
99
107
  }
100
108
  }
109
+
110
+ &-button {
111
+ text-transform: capitalize;
112
+ }
113
+ }
114
+
115
+ button.variant-ghost.app-chart-card-footer-button {
116
+ padding: 0;
117
+ gap: 0;
118
+ min-height: 20px;
119
+
120
+ &:focus-visible {
121
+ border-color: var(--primary);
122
+ @include focus-outline;
123
+ outline-offset: -2px;
124
+ }
101
125
  }
102
126
  </style>
@@ -696,6 +696,7 @@ export default {
696
696
  :content="card.content"
697
697
  :value="card.rawChart"
698
698
  variant="medium"
699
+ role="link"
699
700
  :class="{ 'single-card': appChartCards.length === 1 }"
700
701
  :clickable="true"
701
702
  @card-click="selectChart"
@@ -124,7 +124,7 @@ describe('rcItemCard', () => {
124
124
  }
125
125
  });
126
126
 
127
- const root = wrapper.get(`[data-testid="item-card-${ id }"]`);
127
+ const root = wrapper.get(`[data-testid="card-header-left"]`);
128
128
 
129
129
  expect(root.attributes('role')).toBe('button');
130
130
  expect(root.attributes('tabindex')).toBe('0');
@@ -152,7 +152,9 @@ describe('rcItemCard', () => {
152
152
  }
153
153
  });
154
154
 
155
- await wrapper.trigger('keydown.enter');
155
+ const clickTarget = wrapper.find('.item-card-header-left');
156
+
157
+ await clickTarget.trigger('keydown.enter');
156
158
  expect(wrapper.emitted('card-click')).toBeTruthy();
157
159
  });
158
160
 
@@ -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 {
@@ -4089,6 +4089,20 @@ export function overlayIndividualBanners(parsedBanner: any, banners: any): void;
4089
4089
  // @shell/utils/chart
4090
4090
 
4091
4091
  declare module '@shell/utils/chart' {
4092
+ /**
4093
+ * Compares two chart versions using SemVer logic, with special handling for Rancher's "up" build metadata.
4094
+ *
4095
+ * It uses `semver.compare` for the primary comparison. If versions are considered equal (SemVer ignores build metadata),
4096
+ * 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.
4097
+ *
4098
+ * If the "up" logic doesn't apply or results in equality, it falls back to `semver.compareBuild` to handle
4099
+ * other build metadata differences (e.g. sorting alphabetically).
4100
+ *
4101
+ * @param {string} v1 - The first version string.
4102
+ * @param {string} v2 - The second version string.
4103
+ * @returns {number} - 0 if equal, -1 if v1 < v2, 1 if v1 > v2.
4104
+ */
4105
+ export function compareChartVersions(v1: string, v2: string): number;
4092
4106
  /**
4093
4107
  * Get the latest chart version that is compatible with the cluster's OS and user's pre-release preference.
4094
4108
  * @param {Object} chart - The chart object.
@@ -5408,7 +5422,6 @@ export function parse(str: any): any;
5408
5422
  export function sortable(str: any): any;
5409
5423
  export function compare(in1: any, in2: any): any;
5410
5424
  export function isPrerelease(version?: string): boolean;
5411
- export function isUpgradeFromPreToStable(currentVersion: any, targetVersion: any): any;
5412
5425
  export function isDevBuild(version: any): boolean;
5413
5426
  export function getVersionInfo(store: any): {
5414
5427
  displayVersion: any;
@@ -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.
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})-(.*)$/);